diff --git a/README.md b/README.md index e8c76c4..460d01c 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,16 @@ and encode and send those in to make the device do something in response. +Experimental +==== + +You can try combined.py which is an example of how to switch both the +purple MiHome plugs, and the green button legacy plugs in the same +application (at the moment you have to use the unified_radio branch +to do this, as I haven't merged this to master), but I tried it here +and it works. + + Plans ==== diff --git a/src/Logger.py b/src/Logger.py new file mode 100644 index 0000000..9157c64 --- /dev/null +++ b/src/Logger.py @@ -0,0 +1,85 @@ +# Logger.py 14/05/2016 D.J.Whale +# +# A simple logger - logs to file. + + +from energenie import OpenThings +import os, time + +LOG_FILENAME = "energenie.csv" +HEADINGS = 'timestamp,mfrid,prodid,sensorid,flags,switch,voltage,freq,reactive,real' + + +log_file = None + +def trace(msg): + print(str(msg)) + + +def logMessage(msg): + global log_file + + if log_file == None: + if not os.path.isfile(LOG_FILENAME): + log_file = open(LOG_FILENAME, 'w') + log_file.write(HEADINGS + '\n') + else: + log_file = open(LOG_FILENAME, 'a') # append + + # get the header + header = msg['header'] + timestamp = time.time() + mfrid = header['mfrid'] + productid = header['productid'] + sensorid = header['sensorid'] + + # set defaults for any data that doesn't appear in this message + # but build flags so we know which ones this contains + flags = [0 for i in range(7)] + switch = None + voltage = None + freq = None + reactive = None + real = None + apparent = None + current = None + + # capture any data that we want + ##trace(msg) + for rec in msg['recs']: + paramid = rec['paramid'] + try: + value = rec['value'] + except: + value = None + + if paramid == OpenThings.PARAM_SWITCH_STATE: + switch = value + flags[0] = 1 + elif paramid == OpenThings.PARAM_VOLTAGE: + flags[1] = 1 + voltage = value + elif paramid == OpenThings.PARAM_FREQUENCY: + flags[2] = 1 + freq = value + elif paramid == OpenThings.PARAM_REACTIVE_POWER: + flags[3] = 1 + reactive = value + elif paramid == OpenThings.PARAM_REAL_POWER: + flags[4] = 1 + real = value + elif paramid == OpenThings.PARAM_APPARENT_POWER: + flags[5] = 1 + apparent = value + elif paramid == OpenThings.PARAM_CURRENT: + flags[6] = 1 + current = value + + # generate a line of CSV + flags = "".join([str(a) for a in flags]) + csv = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" % (timestamp, mfrid, productid, sensorid, flags, switch, voltage, freq, reactive, real, apparent, current) + log_file.write(csv + '\n') + log_file.flush() + ##trace(csv) # testing + +# END diff --git a/src/combined.py b/src/combined.py new file mode 100644 index 0000000..5467c22 --- /dev/null +++ b/src/combined.py @@ -0,0 +1,71 @@ +# combined.py 15/05/2016 D.J.Whale +# +# A simple demo of combining both FSK (MiHome) and OOK (green button legacy) +# +# NOTE: This is only a test harness. +# If you really want a nice way to control these devices, wait for the 'device classes' +# issues to be implemented and tested on top of the raw radio interface, as these +# will be much nicer to use. + +import time +from energenie import Messages, OpenThings, radio, encoder, Devices + +# build FSK messages for MiHome purple + +OpenThings.init(Devices.CRYPT_PID) + +PURPLE_ID = 0x68B # captured from a real device using Monitor.py +m = OpenThings.alterMessage( + Messages.SWITCH, + header_sensorid=PURPLE_ID, + recs_0_value=1) +purple_on = OpenThings.encode(m) + +m = OpenThings.alterMessage( + Messages.SWITCH, + header_sensorid=PURPLE_ID, + recs_0_value=0) +purple_off = OpenThings.encode(m) + +# build OOK messages for legacy green button + +GREEN_ON = encoder.build_switch_msg(True, device_address=1) +GREEN_OFF = encoder.build_switch_msg(False, device_address=1) + + +def switch_loop(): + print("Turning green ON") + radio.modulation(ook=True) + radio.transmit(GREEN_ON) + time.sleep(0.5) + + print("Turning purple ON") + radio.modulation(fsk=True) + radio.transmit(purple_on, inner_times=2) + time.sleep(2) + + print("Turning green OFF") + radio.modulation(ook=True) + radio.transmit(GREEN_OFF) + time.sleep(0.5) + + print("Turning purple OFF") + radio.modulation(fsk=True) + radio.transmit(purple_off, inner_times=2) + time.sleep(2) + + +if __name__ == "__main__": + + print("starting combined switch tester") + print("radio init") + radio.init() + + try: + while True: + switch_loop() + + finally: + radio.finished() + +# END diff --git a/src/energenie/Devices.py b/src/energenie/Devices.py index 2730f70..981acf9 100644 --- a/src/energenie/Devices.py +++ b/src/energenie/Devices.py @@ -1,79 +1,91 @@ # Devices.py 30/09/2015 D.J.Whale # # Information about specific Energenie devices +# This table is mostly reverse-engineered from various websites and web catalogues. MFRID = 0x04 +# Deprecated, these are old device names, do not use. +#PRODUCTID_C1_MONITOR = 0x01 # MIHO004 Monitor +#PRODUCTID_R1_MONITOR_AND_CONTROL = 0x02 # MIHO005 Adaptor Plus + #PRODUCTID_MIHO001 = 0x0? # Home Hub #PRODUCTID_MIHO002 = 0x0? # Control only #PRODUCTID_MIHO003 = 0x0? # Hand Controller -PRODUCTID_C1_MONITOR = 0x01 # MIHO004 Monitor -PRODUCTID_R1_MONITOR_AND_CONTROL = 0x02 # MIHO005 Adaptor Plus +PRODUCTID_MIHO004 = 0x01 # Monitor only +PRODUCTID_MIHO005 = 0x02 # Adaptor Plus PRODUCTID_MIHO006 = 0x05 # House Monitor #PRODUCTID_MIHO007 = 0x0? # Double Wall Socket White #PRODUCTID_MIHO008 = 0x0? # Single light switch -#009 not used -#010 not used -#011 not used -#012 not used +#PRODUCTID_MIHO009 not used +#PRODUCTID_MIHO010 not used +#PRODUCTID_MIHO011 not used +#PRODUCTID_MIHO012 not used PRODUCTID_MIHO013 = 0x03 # eTRV #PRODUCTID_MIHO014 = 0x0? # In-line Relay -#015 not used -#016 not used -#017 -#018 -#019 -#020 +#PRODUCTID_MIHO015 not used +#PRODUCTID_MIHO016 not used +#PRODUCTID_MIHO017 +#PRODUCTID_MIHO018 +#PRODUCTID_MIHO019 +#PRODUCTID_MIHO020 #PRODUCTID_MIHO021 = 0x0? # Double Wall Socket Nickel #PRODUCTID_MIHO022 = 0x0? # Double Wall Socket Chrome #PRODUCTID_MIHO023 = 0x0? # Double Wall Socket Brushed Steel #PRODUCTID_MIHO024 = 0x0? # Style Light Nickel #PRODUCTID_MIHO025 = 0x0? # Style Light Chrome #PRODUCTID_MIHO026 = 0x0? # Style Light Steel -#027 starter pack bundle -#028 eco starter pack -#029 heating bundle -#030-036 not used -#037 Adaptor Plus Bundle -#038 2-gang socket Bundle -#039 2-gang socket Bundle black nickel -#040 2-gang socket Bundle chrome -#041 2-gang socket Bundle stainless steel +#PRODUCTID_MIHO027 starter pack bundle +#PRODUCTID_MIHO028 eco starter pack +#PRODUCTID_MIHO029 heating bundle +#PRODUCTID_MIHO030 not used +#PRODUCTID_MIHO031 not used +#PRODUCTID_MIHO032 not used +#PRODUCTID_MIHO033 not used +#PRODUCTID_MIHO034 not used +#PRODUCTID_MIHO035 not used +#PRODUCTID_MIHO036 not used +#PRODUCTID_MIHO037 Adaptor Plus Bundle +#PRODUCTID_MIHO038 2-gang socket Bundle +#PRODUCTID_MIHO039 2-gang socket Bundle black nickel +#PRODUCTID_MIHO040 2-gang socket Bundle chrome +#PRODUCTID_MIHO041 2-gang socket Bundle stainless steel CRYPT_PID = 242 CRYPT_PIP = 0x0100 -# OpenHEMS does not support a broadcast id, but Energenie added one for their -# MiHome Adaptors. This makes simple discovery possible. -BROADCAST_ID = 0xFFFFFF # energenie broadcast +# OpenThings does not support a broadcast id, +# but Energenie added one for their MiHome Adaptors. +# This makes simple discovery possible. +BROADCAST_ID = 0xFFFFFF # Energenie broadcast -# TODO put additional products in here from the Energenie directory -# TODO make this table based +#TODO: put additional products in here from the Energenie directory +#TODO: make this table based def getDescription(mfrid, productid): if mfrid == MFRID: mfr = "Energenie" - if productid == PRODUCTID_C1_MONITOR: - product = "C1 MONITOR" - elif productid == PRODUCTID_R1_MONITOR_AND_CONTROL: + if productid == PRODUCTID_MIHO004: + product = "MIHO004 MONITOR" + elif productid == PRODUCTID_MIHO005: product = "MIHO005 ADAPTOR PLUS" elif productid == PRODUCTID_MIHO006: product = "MIHO006 HOUSE MONITOR" elif productid == PRODUCTID_MIHO013: product = "MIHO013 ETRV" else: - product = "UNKNOWN" + product = "UNKNOWN_%s" % str(hex(productid)) else: - mfr = "UNKNOWN" - product = "UNKNOWN" + mfr = "UNKNOWN_%s" % str(hex(mfrid)) + product = "UNKNOWN_%s" % str(hex(productid)) return "Manufacturer:%s Product:%s" % (mfr, product) def hasSwitch(mfrid, productid): - if mfrid != MFRID: return False - if productid == PRODUCTID_R1_MONITOR_AND_CONTROL: return True + if mfrid != MFRID: return False + if productid == PRODUCTID_MIHO005: return True return False diff --git a/src/energenie/Messages.py b/src/energenie/Messages.py index 7dddd3f..1ab2961 100644 --- a/src/energenie/Messages.py +++ b/src/energenie/Messages.py @@ -8,7 +8,7 @@ SWITCH = { "header": { "mfrid": Devices.MFRID, - "productid": Devices.PRODUCTID_R1_MONITOR_AND_CONTROL, + "productid": Devices.PRODUCTID_MIHO005, "encryptPIP": Devices.CRYPT_PIP, "sensorid": 0 # FILL IN }, @@ -42,4 +42,26 @@ } +REGISTERED_SENSOR = { + "header": { + "mfrid": 0, # FILL IN + "productid": 0, # FILL IN + "encryptPIP": Devices.CRYPT_PIP, + "sensorid": 0 # FILL IN + } +} + + +def send_join_ack(radio, mfrid, productid, sensorid): + # send back a JOIN ACK, so that join light stops flashing + response = OpenThings.alterMessage(JOIN_ACK, + header_mfrid=mfrid, + header_productid=productid, + header_sensorid=sensorid) + p = OpenThings.encode(response) + radio.transmitter() + radio.transmit(p, inner_times=2) + radio.receiver() + + # END diff --git a/src/energenie/OpenThings.py b/src/energenie/OpenThings.py index fbf935b..7dc3862 100644 --- a/src/energenie/OpenThings.py +++ b/src/energenie/OpenThings.py @@ -622,7 +622,7 @@ def getFromMessage(message, keypath): - """Get a field from a message, given a keypath to the item""" + """Get a field from a message, given an underscored keypath to the item""" path = keypath.split("_") for p in path[:-1]: @@ -630,8 +630,8 @@ p = int(p) except: pass - m = m[p] - return m[path[-1]] + message = message[p] + return message[path[-1]] #----- TEST HARNESS ----------------------------------------------------------- diff --git a/src/energenie/Registry.py b/src/energenie/Registry.py new file mode 100644 index 0000000..f2316b0 --- /dev/null +++ b/src/energenie/Registry.py @@ -0,0 +1,50 @@ +# Registry.py 14/05/2016 D.J.Whale +# +# A simple registry of connected devices. +# +# NOTE: This is an initial, non persisted implementation only + +import time +import Devices + +directory = {} + +def allkeys(d): + result = "" + for k in d: + if len(result) != 0: + result += ',' + result += str(k) + return result + + +def update(message): + """Update the local directory with information about this device""" + now = time.time() + header = message["header"] + sensorId = header["sensorid"] + + if not directory.has_key(sensorId): + # new device discovered + desc = Devices.getDescription(header["mfrid"], header["productid"]) + print("ADD device:%s %s" % (hex(sensorId), desc)) + directory[sensorId] = {"header": message["header"]} + #trace(allkeys(directory)) + + directory[sensorId]["time"] = now + #TODO would be good to keep recs, but need to iterate through all and key by paramid, + #not as a list index, else merging will be hard. + + +def size(): + return len(directory) + + +def get_sensorids(): + return directory.keys() + + +def get_info(sensor_id): + return directory[sensor_id] + +# END diff --git a/src/energenie/drv/build_mac b/src/energenie/drv/build_mac index e96ac74..379e8d4 100755 --- a/src/energenie/drv/build_mac +++ b/src/energenie/drv/build_mac @@ -10,9 +10,9 @@ # build spis_test (soft SPI tester) -##gcc spi_test.c spis.c gpio_sim.c delay_posix.c -##mv a.out spis_test -##chmod u+x spis_test +gcc spi_test.c spis.c gpio_sim.c delay_posix.c +mv a.out spis_test +chmod u+x spis_test # build hrf69_test @@ -40,13 +40,7 @@ chmod u+x radio_test -# build spi .so library on Raspberry Pi -##gcc -Wall -shared -o spi_rpi.so -fPIC spis_rpi.c gpio_rpi.c -##nm -D spi_rpi.so -##cp spi_rpi.so .. - - -# radio spi .so library on Mac +# radio .so library on Mac gcc -Wall -shared -o radio_mac.so -fPIC radio.c hrfm69.c spis.c gpio_sim.c delay_posix.c ##nm -g radio_mac.so ##cp radio_mac.so .. diff --git a/src/energenie/drv/build_rpi b/src/energenie/drv/build_rpi index b7076c2..a10bfa0 100755 --- a/src/energenie/drv/build_rpi +++ b/src/energenie/drv/build_rpi @@ -31,15 +31,9 @@ mv a.out radio_test chmod u+x radio_test -# build spi .so library on Raspberry Pi -##gcc -Wall -shared -o spi_rpi.so -fPIC spis_rpi.c gpio_rpi.c -# nm -D spi_rpi.so -##cp spi_rpi.so .. - -# radio spi .so library on Raspberry Pi +# radio .so library on Raspberry Pi gcc -Wall -shared -o radio_rpi.so -fPIC radio.c hrfm69.c spis.c gpio_rpi.c delay_posix.c ##nm -D radio_rpi.so -cp radio_rpi.so .. # END diff --git a/src/energenie/drv/hrfm69.c b/src/energenie/drv/hrfm69.c index 6960e45..cb15335 100644 --- a/src/energenie/drv/hrfm69.c +++ b/src/energenie/drv/hrfm69.c @@ -58,26 +58,79 @@ /*---------------------------------------------------------------------------*/ +// Read bytes from FIFO in burst mode. +// Never reads more than buflen bytes +// First received byte is the count of remaining bytes +// That byte is also returned in the user buffer. +// Note the user buffer can be > FIFO_MAX, but there is no flow control +// in the HRF driver yet, so you might get an underflow error if data is read +// quicker than it comes in on-air. You might get an overflow error if +// data comes in quicker than it is read. -void HRF_readfifo_burst(uint8_t* buf, uint8_t len) + +HRF_RESULT HRF_readfifo_burst_cbp(uint8_t* buf, uint8_t buflen) { -//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() -// return buf + uint8_t data; + + spi_select(); + spi_byte(HRF_ADDR_FIFO); /* prime the fifo burst reader */ + + /* Read the first byte, and then decide how many remaining bytes to receive */ + data = spi_byte(HRF_ADDR_FIFO); + *(buf++) = data; /* the count byte is always returned as first byte of user buffer */ + + /* Validate the payload len against the supplied user buffer */ + if (data > buflen) + { + spi_deselect(); + TRACE_OUTS("buffer too small for payload len="); + TRACE_OUTN(data); + TRACE_NL(); + return HRF_RESULT_ERR_BUFFER_TOO_SMALL; + } + + buflen = data; /* now the expected payload length */ + + while (buflen != 0) + { + data = spi_byte(HRF_ADDR_FIFO); + *(buf++) = data; + buflen--; + } + spi_deselect(); + + //TODO: Read irqflags + //if underflow, this is an error (reading out too quick) + //if overflow, this is an error (not reading out quick enough) + //if not empty at end, this is a warning (might be ok, but user might want to clear_fifo after) + return HRF_RESULT_OK; +} + + +/*---------------------------------------------------------------------------*/ +// Read bytes from FIFO in burst mode. +// Tries to read exactly buflen bytes + +HRF_RESULT HRF_readfifo_burst_len(uint8_t* buf, uint8_t buflen) +{ + uint8_t data; + + spi_select(); + spi_byte(HRF_ADDR_FIFO); /* prime the fifo burst reader */ + + while (buflen != 0) + { + data = spi_byte(HRF_ADDR_FIFO); + *(buf++) = data; + buflen--; + } + spi_deselect(); + + //TODO: Read irqflags + //if underflow, this is an error (reading out too quick) + //if overflow, this is an error (not reading out quick enough) + //if not empty at end, this is a warning (might be ok, but user might want to clear_fifo after) + return HRF_RESULT_OK; } @@ -87,7 +140,11 @@ HRF_RESULT HRF_checkreg(uint8_t addr, uint8_t mask, uint8_t value) { uint8_t regval = HRF_readreg(addr); - return (regval & mask) == value; + if ((regval & mask) == value) + { + return HRF_RESULT_OK_TRUE; + } + return HRF_RESULT_OK_FALSE; } @@ -104,7 +161,8 @@ while (! HRF_checkreg(addr, mask, value)) { - // busy wait (TODO:with no timeout & error recovery?) + // busy wait + //TODO: No timeout or error recovery? Can cause permanent lockup } } @@ -115,6 +173,7 @@ void HRF_clear_fifo(void) { //TODO: max fifolen is 66, should bail after that to prevent lockup + //especially if radio crashed and SPI always returns stuck flag bit while ((HRF_readreg(HRF_ADDR_IRQFLAGS2) & HRF_MASK_FIFONOTEMPTY) == HRF_MASK_FIFONOTEMPTY) { HRF_readreg(HRF_ADDR_FIFO); diff --git a/src/energenie/drv/hrfm69.h b/src/energenie/drv/hrfm69.h index 3223fe8..aab23a0 100644 --- a/src/energenie/drv/hrfm69.h +++ b/src/energenie/drv/hrfm69.h @@ -13,6 +13,11 @@ #include "system.h" typedef uint8_t HRF_RESULT; +//consider these, so we can easily pass back a boolean too +#define HRF_RESULT_OK 0x00 +#define HRF_RESULT_OK_FALSE 0x00 +#define HRF_RESULT_OK_TRUE 0x01 +#define HRF_RESULT_ERR_BUFFER_TOO_SMALL 0x81 typedef struct { @@ -113,7 +118,9 @@ extern void HRF_writefifo_burst(uint8_t* buf, uint8_t len); -extern void HRF_readfifo_burst(uint8_t* buf, uint8_t len); +extern HRF_RESULT HRF_readfifo_burst_cbp(uint8_t* buf, uint8_t buflen); + +extern HRF_RESULT HRF_readfifo_burst_len(uint8_t* buf, uint8_t buflen); extern HRF_RESULT HRF_checkreg(uint8_t addr, uint8_t mask, uint8_t value); diff --git a/src/energenie/drv/radio.c b/src/energenie/drv/radio.c index b8ea7ec..81a408f 100644 --- a/src/energenie/drv/radio.c +++ b/src/energenie/drv/radio.c @@ -6,6 +6,67 @@ */ +/* TODO (rx) +DONE: decide interface for: HRF_readfifo_burst(uint8_t* buf, uint8_t len) +DONE: Knit in the payload check +DONE: Knit in the get_payload + +TODO: test with monitor.py (receive only mode, FSK) +*/ + + +/* TODO (OOK rx) +TODO: Write a tester legacy_rx.py to exercise 16 byte OOK receive +TODO: See if we can receive from RF hand transmiter +TODO: See how much 'noise bytes' we get +TODO: Might need to set the sync bytes on OOK receive to prevent false trigger + This will mean loading a different configuration for OOK receive to transmit + But we don't want to be manually looking for sync bits, and we don't want + to use the preamble generator on tx as it doesn't work too well. +*/ + + +/* +TODO: Harden up the payload receiver + 1. If there is a corrupted packet in the fifo, the first byte + might not be the length byte, and the read might fail or leave + bytes in the buffer. So, the receiver needs to be hardended up + to cope with these cases. HRF_readfifo_burst returns an error + if the length byte is longer than buflen, but need to check this + error code. + + 2. if we are in fixed length, or count byte, and more data + is in the fifo than we expect, this will be read next time. + Perhaps we need to clear the fifo at the end of each read operation, + and also report if there is junk in the buffer (i.e. would always + expect a read to leave an empty fifo). + + 3. We should check the fifo underrun bit at the end of a read, + this would mean that some of the bytes we read are not real bytes + and should not be interpreted, that packet should be junked. +*/ + + +/* TODO: DUTY CYCLE PROTECTION REQUIREMENT + * + * See page 3 of this app note: http://www.ti.com/lit/an/swra090/swra090.pdf + * + * At OOK 4800bps, 1 bit is 20uS, 1 byte is 1.6ms, 16 bytes is 26.6ms + * 15 times (old design limit) is 400ms + * 255 times (new design limit) is 6.8s + * + * Transmitter duty cycle + * The transmitter duty cycle is defined as the ratio of the maximum ”on” time, + * relative to a onehour period. If message acknowledgement is required, the + * additional ”on” time shall be included. Advisory limits are: + * + * Duty cycle Maximum “on” time [sec] Minimum “off” time [sec] + * 0.1 % 0.72 0.72 + * 1 % 3.6 1.8 + * 10 % 36 3.6 + */ + + /***** INCLUDES *****/ #include "system.h" @@ -20,16 +81,15 @@ /***** CONFIGURATION *****/ #define EXPECTED_RADIOVER 36 +#define MAX_FIFO_BUFFER 66 // Energenie specific radio config values -//#define RADIO_VAL_SYNCVALUE1FSK 0x2D // 1st byte of Sync word -//#define RADIO_VAL_SYNCVALUE2FSK 0xD4 // 2nd byte of Sync word -//#define RADIO_VAL_SYNCVALUE1OOK 0x80 // 1nd byte of Sync word +#define RADIO_VAL_SYNCVALUE1FSK 0x2D // 1st byte of Sync word +#define RADIO_VAL_SYNCVALUE2FSK 0xD4 // 2nd byte of Sync word +#define RADIO_VAL_SYNCVALUE1OOK 0x80 // 1nd byte of Sync word //#define RADIO_VAL_PACKETCONFIG1FSK 0xA2 // Variable length, Manchester coding, Addr must match NodeAddress -//#define RADIO_VAL_PACKETCONFIG1FSKNO 0xA0 // Variable length, Manchester coding -//#define RADIO_VAL_PACKETCONFIG1OOK 0 // Fixed length, no Manchester coding -//#define RADIO_VAL_PAYLOADLEN_OOK (13 + 8 * 17) // Payload Length (WRONG!) +#define RADIO_VAL_PACKETCONFIG1FSKNO 0xA0 // Variable length, Manchester coding //TODO: Not sure, might pass this in? What about on Arduino? //What about if we have multiple chip selects on same SPI? @@ -56,32 +116,31 @@ static void _wait_ready(void); static void _wait_txready(void); static void _config(HRF_CONFIG_REC* config, uint8_t len); -//static int _payload_waiting(void); +static int _payload_waiting(void); //----- ENERGENIE SPECIFIC CONFIGURATIONS -------------------------------------- -// 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_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_PACKETCONFIG1FSKNO], # Variable length, Manchester coding -// [ADDR_PAYLOADLEN, VAL_PAYLOADLEN66], # max Length in RX, not used in Tx -// [ADDR_NODEADDRESS, 0x06], # Node address used in address filtering TODO??? -// [ADDR_FIFOTHRESH, VAL_FIFOTHRESH1], # Condition to start packet transmission: at least one byte in FIFO -// [ADDR_OPMODE, MODE_RECEIVER] # Operating mode to Receiver -// ] -//#define CONFIG_FSK_COUNT (sizeof(config_FSK)/sizeof(HRF_CONFIG_REC)) + +static HRF_CONFIG_REC config_FSK[] = { + {HRF_ADDR_REGDATAMODUL, HRF_VAL_REGDATAMODUL_FSK}, // modulation scheme FSK + {HRF_ADDR_FDEVMSB, HRF_VAL_FDEVMSB30}, // frequency deviation 5kHz 0x0052 -> 30kHz 0x01EC + {HRF_ADDR_FDEVLSB, HRF_VAL_FDEVLSB30}, // frequency deviation 5kHz 0x0052 -> 30kHz 0x01EC + {HRF_ADDR_FRMSB, HRF_VAL_FRMSB434}, // carrier freq -> 434.3MHz 0x6C9333 + {HRF_ADDR_FRMID, HRF_VAL_FRMID434}, // carrier freq -> 434.3MHz 0x6C9333 + {HRF_ADDR_FRLSB, HRF_VAL_FRLSB434}, // carrier freq -> 434.3MHz 0x6C9333 + {HRF_ADDR_AFCCTRL, HRF_VAL_AFCCTRLS}, // standard AFC routine + {HRF_ADDR_LNA, HRF_VAL_LNA50}, // 200ohms, gain by AGC loop -> 50ohms + {HRF_ADDR_RXBW, HRF_VAL_RXBW60}, // channel filter bandwidth 10kHz -> 60kHz page:26 + {HRF_ADDR_BITRATEMSB, 0x1A}, // 4800b/s + {HRF_ADDR_BITRATELSB, 0x0B}, // 4800b/s + {HRF_ADDR_SYNCCONFIG, HRF_VAL_SYNCCONFIG2}, // Size of the Synch word = 2 (SyncSize + 1) + {HRF_ADDR_SYNCVALUE1, RADIO_VAL_SYNCVALUE1FSK}, // 1st byte of Sync word + {HRF_ADDR_SYNCVALUE2, RADIO_VAL_SYNCVALUE2FSK}, // 2nd byte of Sync word + {HRF_ADDR_PACKETCONFIG1, RADIO_VAL_PACKETCONFIG1FSKNO}, // Variable length, Manchester coding + {HRF_ADDR_PAYLOADLEN, HRF_VAL_PAYLOADLEN66}, // max Length in RX, not used in Tx + {HRF_ADDR_NODEADDRESS, 0x06}, // Node address used in address filtering (not used) +}; +#define CONFIG_FSK_COUNT (sizeof(config_FSK)/sizeof(HRF_CONFIG_REC)) static HRF_CONFIG_REC config_OOK[] = { @@ -177,15 +236,11 @@ /*---------------------------------------------------------------------------*/ // Check if there is a payload in the FIFO waiting to be processed -//static int _payload_waiting(void) -//{ -// //TODO: First read might be superflous, but left in just in case -// //uint8_t irqflags1 = -// HRF_readreg(HRF_ADDR_IRQFLAGS1); -// -// uint8_t irqflags2 = HRF_readreg(HRF_ADDR_IRQFLAGS2); -// return (irqflags2 & HRF_MASK_PAYLOADRDY) == HRF_MASK_PAYLOADRDY; -//} +static int _payload_waiting(void) +{ + uint8_t irqflags2 = HRF_readreg(HRF_ADDR_IRQFLAGS2); + return (irqflags2 & HRF_MASK_PAYLOADRDY) == HRF_MASK_PAYLOADRDY; +} /***** PUBLIC ****************************************************************/ @@ -256,11 +311,11 @@ _config(config_OOK, CONFIG_OOK_COUNT); radio_data.modu = mod; } - //else if (mod == RADIO_MODULATION_FSK) - //{ - // _config(config_FSK, CONFIG_FSK_COUNT); - // radio_data.modu = mod; - //} + else if (mod == RADIO_MODULATION_FSK) + { + _config(config_FSK, CONFIG_FSK_COUNT); + radio_data.modu = mod; + } else //TODO: make this ASSERT() { TRACE_FAIL("Unknown modulation\n"); @@ -269,6 +324,7 @@ /*---------------------------------------------------------------------------*/ +// Put radio into transmit mode for chosen modulation scheme void radio_transmitter(RADIO_MODULATION mod) { @@ -280,6 +336,9 @@ /*---------------------------------------------------------------------------*/ +// Put radio into receive mode for chosen modulation scheme +// This will open up the receive window, so that a packet can come in +// and be detected later by radio_isReceiveWaiting void radio_receiver(RADIO_MODULATION mod) { @@ -323,38 +382,6 @@ /*---------------------------------------------------------------------------*/ // Send a payload of data -/* DESIGN FOR DUTY CYCLE PROTECTION REQUIREMENT (write this later) - * - * At OOK 4800bps, 1 bit is 20uS, 1 byte is 1.6ms, 16 bytes is 26.6ms - * 15 times (old design limit) is 400ms - * 255 times (new design limit) is 6.8s - - * See page 3 of this app note: http://www.ti.com/lit/an/swra090/swra090.pdf - * - * Transmitter duty cycle - * The transmitter duty cycle is defined as the ratio of the maximum ”on” time, relative to a onehour period. - * If message acknowledgement is required, the additional ”on” time shall be included. Advisory limits are: - * - * Duty cycle Maximum “on” time [sec] Minimum “off” time [sec] - * 0.1 % 0.72 0.72 - * 1 % 3.6 1.8 - * 10 % 36 3.6 - */ - -/* DESIGN FOR >255 payload len (write this later) - - will need to set fifolevel as a proportion of payload len - and load that proportion. - i.e. inside the payload repeat loop - load the fifo up in non integral payload portions - also, txstart condition would need to start before whole payload loaded in FIFO - that is probably ok, but fifolev is more to do with fill rate and transmit rate, - and less to do with the actual payload length. - Note that FIFO empties at a rate proportional to the bitrate, - and also adding on manchester coding will slow the emptying rate. - */ - - void radio_send_payload(uint8_t* payload, uint8_t len, uint8_t times) { TRACE_OUTS("radio_send_payload\n"); @@ -417,7 +444,7 @@ TRACE_OUTN(irqflags2); TRACE_NL(); - //TODO: make this ASSERT()?? + //TODO: make this TRACE_ASSERT() if (((irqflags2 & HRF_MASK_FIFONOTEMPTY) != 0) || ((irqflags2 & HRF_MASK_FIFOOVERRUN) != 0)) { TRACE_FAIL("FIFO not empty or overrun at end of burst"); @@ -427,39 +454,62 @@ /*---------------------------------------------------------------------------*/ +// Check to see if a payload is waiting in the receive buffer -//RADIO_RESULT radio_isReceiveWaiting(void) -//{ -// def isReceiveWaiting(): -// """Check to see if a payload is waiting in the receive buffer""" -// return check_payload() -// return RADIO_RESULT_ERR_UNIMPLEMENTED; -//} +RADIO_RESULT radio_is_receive_waiting(void) +{ + if (_payload_waiting()) + { + return RADIO_RESULT_OK_TRUE; + } + return RADIO_RESULT_OK_FALSE; +} /*---------------------------------------------------------------------------*/ -//TODO: high level receive, put into receive, receive a payload, back to standby +// read a single payload from the payload buffer +// this reads a fixed length payload -//RADIO_RESULT radio_receive(uint8_t* buf, uint8_t len) -//{ -// def receive(): -// """Receive a single payload from the buffer using the present modulation scheme""" -// change into receive mode if not already there -// receive payload -// if was not in receive mode, change back to previous mode -//} - +RADIO_RESULT radio_get_payload_len(uint8_t* buf, uint8_t buflen) +{ + if (buflen > MAX_FIFO_BUFFER) + { /* At the moment, the receiver cannot reliably cope with payloads > 1 FIFO buffer. + * It *might* be able to in the future. + */ + return RADIO_RESULT_ERR_LONG_PAYLOAD; + } + HRF_RESULT r = HRF_readfifo_burst_len(buf, buflen); + if (r != HRF_RESULT_OK) + { + return RADIO_RESULT_ERR_READ_FAILED; + } + return RADIO_RESULT_OK; +} /*---------------------------------------------------------------------------*/ -//TODO: low level receive, just receive a payload -// -//RADIO_RESULT radio_receive_payload(uint8_t* buf, uint8_t len) -//{ -// def receive(): -// """Receive a single payload from the buffer using the present modulation scheme""" -// return HRF_receive_payload() -// return RADIO_RESULT_ERR_UNIMPLEMENTED; -//} +// read a single payload from the payload buffer +// this reads count byte preceeded payloads. +// The CBP payload always has the count byte in the first byte +// and this is returned in the user buffer too. + +RADIO_RESULT radio_get_payload_cbp(uint8_t* buf, uint8_t buflen) +{ + ////if (buflen > MAX_FIFO_BUFFER) + ////{ /* At the moment, the receiver cannot reliably cope with payloads > 1 FIFO buffer. + //// * It *might* be able to in the future. + //// */ + //// return RADIO_RESULT_ERR_LONG_PAYLOAD; + ////} + HRF_RESULT r = HRF_readfifo_burst_cbp(buf, buflen); + if (r != HRF_RESULT_OK) + { + TRACE_OUTS("radio_get_payload_cbp failed, error="); + TRACE_OUTN(r); + TRACE_NL(); + return RADIO_RESULT_ERR_READ_FAILED; + } + return RADIO_RESULT_OK; +} /*---------------------------------------------------------------------------*/ diff --git a/src/energenie/drv/radio.h b/src/energenie/drv/radio.h index deb0168..7097075 100644 --- a/src/energenie/drv/radio.h +++ b/src/energenie/drv/radio.h @@ -8,7 +8,13 @@ #define _RADIO_H typedef uint8_t RADIO_RESULT; -#define RADIO_RESULT_ERR_UNIMPLEMENTED 0x81 +#define RADIO_RESULT_IS_ERR(R) (((R) & 0x80) != 0) +#define RADIO_RESULT_OK 0x00 +#define RADIO_RESULT_OK_FALSE 0x00 +#define RADIO_RESULT_OK_TRUE 0x01 +#define RADIO_RESULT_ERR_UNIMPLEMENTED 0x80 +#define RADIO_RESULT_ERR_LONG_PAYLOAD 0x81 +#define RADIO_RESULT_ERR_READ_FAILED 0x82 typedef uint8_t RADIO_MODULATION; #define RADIO_MODULATION_OOK 0 @@ -29,13 +35,13 @@ extern void radio_send_payload(uint8_t* payload, uint8_t len, uint8_t times); -//extern void radio_receiver(RADIO_MODULATION mod); +extern void radio_receiver(RADIO_MODULATION mod); -//extern RADIO_RESULT radio_isReceiveWaiting(void); +extern RADIO_RESULT radio_is_receive_waiting(void); -//extern RADIO_RESULT radio_receive(uint8_t* buf, uint8_t len); +extern RADIO_RESULT radio_get_payload_len(uint8_t* buf, uint8_t buflen); -//extern RADIO_RESULT radio_receive_payload(uint8_t* buf, uint8_t len); +extern RADIO_RESULT radio_get_payload_cbp(uint8_t* buf, uint8_t buflen); extern void radio_standby(void); diff --git a/src/energenie/drv/radio_rpi.so b/src/energenie/drv/radio_rpi.so index 0129587..b18de90 100755 --- a/src/energenie/drv/radio_rpi.so +++ b/src/energenie/drv/radio_rpi.so Binary files differ diff --git a/src/energenie/drv/spi_test.c b/src/energenie/drv/spi_test.c index cef2dc1..a371957 100644 --- a/src/energenie/drv/spi_test.c +++ b/src/energenie/drv/spi_test.c @@ -14,6 +14,7 @@ #include "gpio.h" #include "spi.h" #include "trace.h" +#include "delay.h" /***** CONSTANTS *****/ diff --git a/src/energenie/drv/spih_arduino.c b/src/energenie/drv/spih_arduino.c deleted file mode 100644 index 74643ce..0000000 --- a/src/energenie/drv/spih_arduino.c +++ /dev/null @@ -1,6 +0,0 @@ -// placeholder for arduino SPI hardware driver -// -// This will be used to aid porting to the arduino. -// The arduino has more predictable timing than the Raspberry Pi, -// and thus it is a much better test suite platform. - diff --git a/src/energenie/radio.py b/src/energenie/radio.py index 83c6f6a..6b6f294 100644 --- a/src/energenie/radio.py +++ b/src/energenie/radio.py @@ -1,493 +1,271 @@ -# test1.py 26/09/2015 D.J.Whale +# radio2.py 15/04/2015 D.J.Whale # -# Simple low level test of the HopeRF interface -# Uses direct SPI commands to exercise the interface. +# New version of the radio driver, with most of the fast stuff pushed into C. # -# 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. +# NOTE 1: This is only used for OOK transmit & FSK transmit at the moment. +# FSK receive is currently being re-implemented in radio.c -import spi +# NOTE 2: Also there is an idea to do a python wrapper, build the C code +# for an Arduino and wrap it with a simple serial message handler. +# This would then make it possible to use the Energenie Radio on a Mac/PC/Linux +# machine but by still using the same higher level Python code. +# All you would need is a different radio.py that marshalled data to and from +# the Arduino via pyserial. -def warning(msg): - print("warning:" + str(msg)) +#TODO: Should really add parameter validation here, so that C code doesn't have to. +#although it will be faster in C (C could be made optional, like an assert?) + +LIBNAME = "drv/radio_rpi.so" +##LIBNAME = "drv/radio_mac.so" # testing + +import time +import ctypes +from os import path +mydir = path.dirname(path.abspath(__file__)) + +libradio = ctypes.cdll.LoadLibrary(mydir + "/" + LIBNAME) +radio_init_fn = libradio["radio_init"] +radio_reset_fn = libradio["radio_reset"] +radio_get_ver_fn = libradio["radio_get_ver"] +radio_modulation_fn = libradio["radio_modulation"] +radio_transmitter_fn = libradio["radio_transmitter"] +radio_transmit_fn = libradio["radio_transmit"] +radio_send_payload_fn = libradio["radio_send_payload"] +radio_receiver_fn = libradio["radio_receiver"] +radio_is_receive_waiting_fn = libradio["radio_is_receive_waiting"] +radio_get_payload_len_fn = libradio["radio_get_payload_len"] +radio_get_payload_cbp_fn = libradio["radio_get_payload_cbp"] +radio_standby_fn = libradio["radio_standby"] +radio_finished_fn = libradio["radio_finished"] + +RADIO_MODULATION_OOK = 0 +RADIO_MODULATION_FSK = 1 + +# A temporary limit, the receiver will only receive 1 FIFO worth of data maximul +# This includes the length byte at the start of an OpenThings message +MAX_RX_SIZE = 66 + + +#TODO RADIO_RESULT_XX def trace(msg): print(str(msg)) -def ashex(p): +def tohex(l): line = "" - for b in p: - line += str(hex(b)) + " " + for item in l: + line += hex(item) + " " return line -#----- HOPERF REGISTER INTERFACE ---------------------------------------------- -# Precise register descriptions can be found in: -# 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_VERSION = 0x10 -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 -##TODO: This calculation looks wrong, inherited from the original C code. -#It accounts for the 'magic' byte that C used for the SPI address. -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 two? bytes in FIFO -VAL_FIFOTHRESH30 = 0x1E # Condition to start packet transmission: wait for >30 bytes in FIFO +def unimplemented(m): + print("warning: method is not implemented:%s" % m) + return m -#----- HOPERF RADIO INTERFACE ------------------------------------------------- - -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 deprecated(m): + """Load-time warning about deprecated method""" + print("warning: method is deprecated:%s" % m) + return m -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 untested(m): + """Load-time warning about untested function""" + print("warning: method is untested:%s" % m) + return m -def HRF_writefifo_burst(buf): - """Write all bytes in buf to the payload FIFO, in a single burst""" - # Don't modify buf, in case caller reuses it - txbuf = [ADDR_FIFO | MASK_WRITE_DATA] - for b in buf: - txbuf.append(b) - #print("write FIFO %s" % ashex(txbuf)) +def disabled(m): + """Load-time waring about disabled function""" + print("warning: method is disabled:%s" % m) + def nothing(*args, **kwargs):pass + return nothing - spi.select() - spi.frame(txbuf) - 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(ashex(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 - - -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_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) - #trace("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") - #trace("payload:%s" % ashex(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") - - - -#----- ENERGENIE SPECIFIC CONFIGURATIONS -------------------------------------- - -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 -] - -config_OOK = [ - [ADDR_REGDATAMODUL, VAL_REGDATAMODUL_OOK], # modulation scheme OOK - [ADDR_FDEVMSB, 0], # frequency deviation -> 0kHz - [ADDR_FDEVLSB, 0], # frequency deviation -> 0kHz - [ADDR_FRMSB, VAL_FRMSB433], # carrier freq -> 433.92MHz 0x6C7AE1 - [ADDR_FRMID, VAL_FRMID433], # carrier freq -> 433.92MHz 0x6C7AE1 - [ADDR_FRLSB, VAL_FRLSB433], # carrier freq -> 433.92MHz 0x6C7AE1 - [ADDR_RXBW, VAL_RXBW120], # channel filter bandwidth 120kHz - [ADDR_BITRATEMSB, 0x1A], # 4800b/s - [ADDR_BITRATELSB, 0x0B], # 4800b/s - [ADDR_PREAMBLELSB, 0], # preamble size LSB 3 - [ADDR_SYNCCONFIG, VAL_SYNCCONFIG4], # Size of the Sync word = 4 (SyncSize + 1) - [ADDR_SYNCVALUE1, VAL_SYNCVALUE1OOK], # sync value 1 - [ADDR_SYNCVALUE2, 0], # sync value 2 - [ADDR_SYNCVALUE3, 0], # sync value 3 - [ADDR_SYNCVALUE4, 0], # sync value 4 - [ADDR_PACKETCONFIG1, VAL_PACKETCONFIG1OOK], # Fixed length, no Manchester coding, OOK - [ADDR_PAYLOADLEN, VAL_PAYLOADLEN_OOK], # Payload Length - [ADDR_FIFOTHRESH, VAL_FIFOTHRESH30], # Condition to start packet transmission: wait for 30 bytes in FIFO -] - - -def HRF_config(config): - """Load a table of configuration values into HRF registers""" - for cmd in config: - HRF_writereg(cmd[0], cmd[1]) - HRF_wait_ready() - - -#ORIGINAL C CODE -#void HRF_send_OOK_msg(uint8_t relayState) -#{ -# uint8_t buf[17]; -# uint8_t i; -# -# HRF_config_OOK(); -# -# buf[1] = 0x80; // Preambule 32b enclosed in sync words -# buf[2] = 0x00; -# buf[3] = 0x00; -# buf[4] = 0x00; -# -# for (i = 5; i <= 14; ++i){ -# buf[i] = 8 + (i&1) * 6 + 128 + (i&2) * 48; // address 20b * 4 = 10 Bytes -# } -# -# if (relayState == 1) -# { -# printf("relay ON\n\n"); -# buf[15] = 0xEE; // D0-high, D1-h // S1 on -# buf[16] = 0xEE; // D2-h, D3-h -# } -# else -# { -# printf("relay OFF\n\n"); -# buf[15] = 0xEE; // D0-high, D1-h // S1 off -# buf[16] = 0xE8; // D2-h, D3-l -# } -# -# HRF_wait_for (ADDR_IRQFLAGS1, MASK_MODEREADY | MASK_TXREADY, true); // wait for ModeReady + TX ready -# HRF_reg_Wn(buf + 4, 0, 12); // don't include sync word (4 bytes) into data buffer -# -# for (i = 0; i < 8; ++i) // Send the same message few more times -# { -# HRF_wait_for(ADDR_IRQFLAGS2, MASK_FIFOLEVEL, false); -# HRF_reg_Wn(buf, 0, 16); // with sync word -# } -# -# HRF_wait_for (ADDR_IRQFLAGS2, MASK_PACKETSENT, true); // wait for Packet sent -# HRF_assert_reg_val(ADDR_IRQFLAGS2, MASK_FIFONOTEMPTY | MASK_FIFOOVERRUN, false, "are all bytes sent?"); -# HRF_config_FSK(); -# HRF_wait_for (ADDR_IRQFLAGS1, MASK_MODEREADY, true); // wait for ModeReady -#} - - -# first payload -# (radio sync 4 bytes, not counted) -# address 10 bytes -# command 2 bytes -# i.e. 12 bytes -# so, >30 is 2 and a bit payloads loaded. -# 66 byte FIFO size -# so that means FIFO roughly half full before starts tx, -# FIFO at or below half full before another payload will be added - -# packetsent is based on the fixed payload length -# which is (13 + 8 * 17) = 149 -# This maths looks wrong. -# 10 bytes of address, two bytes of command = 12 -# 13 includes the dummy byte at the start, but that is for the SPI address and not counted - -def HRF_send_OOK_payload(payload): - """Send a payload multiple times""" - - #TODO: note the zero at the start was the C method of reserving space for address byte - p1 = [0x00] + payload - # This sync pattern does not match C code, but it works. - # The sync pattern from the C code does not work here - # Note that buf[0] in the C is undefined due to being uninitialised - # but it is a space for the address byte in the C fifo burst routine - pn = [0x00,0x80,0x00,0x00,0x00] + payload # from the C - #TODO: note the zero at the start was the C method of reserving space for address byte - # which is not needed here in this python. - # This is old test data due to wrong baud rate - deprecated - #pn = [0x80,0x80,0x80,0x80,0x80] + payload - - HRF_pollreg(ADDR_IRQFLAGS1, MASK_MODEREADY|MASK_TXREADY, MASK_MODEREADY|MASK_TXREADY) - HRF_writefifo_burst(p1) - - for i in range(8): - # waits for <31 bytes in FIFO - HRF_pollreg(ADDR_IRQFLAGS2, MASK_FIFOLEVEL, 0) - HRF_writefifo_burst(pn) - - HRF_pollreg(ADDR_IRQFLAGS2, MASK_PACKETSENT, MASK_PACKETSENT) # wait for Packet 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 repeated payload to HRF") - - # Note: packetsent is only cleared on exit from TX (i.e to STANDBY or RECEIVE) - - - -#----- RADIO API -------------------------------------------------------------- - -mode = None -modulation_fsk = None def init(): """Initialise the module ready for use""" - spi.init_defaults() - trace("RESET") - - # Note that if another program left GPIO pins in a different state - # and did a dirty exit, the reset fails to work and the clear fifo hangs. - # Might have to make the spi.init() set everything to inputs first, - # then set to outputs, to make sure that the - # GPIO registers are in a deterministic start state. - spi.reset() # send a hardware reset to ensure radio in clean state - - HRF_clear_fifo() + #extern void radio_init(void); + radio_init_fn() def reset(): - """Reset the radio chip""" - spi.reset() + """Reset the radio device""" + #extern void radio_reset(void); + radio_reset_fn() def get_ver(): - """Get the version number of the radio chip""" - return HRF_readreg(ADDR_VERSION) + """Read out the version number of the radio""" + return radio_get_ver_fn() def modulation(fsk=None, ook=None): """Switch modulation, if needed""" - global modulation_fsk - - # Handle sensible module defaults for earlier versions of user code - if fsk == None and ook == None: - # Force FSK mode - fsk = True - - if fsk != None and fsk: - if modulation_fsk == None or modulation_fsk == False: - trace("switch to FSK") - HRF_config(config_FSK) - modulation_fsk = True - - elif ook != None and ook: - if modulation_fsk == None or modulation_fsk == True: - trace("switch to OOK") - HRF_config(config_OOK) - modulation_fsk = False + #extern void radio_modulation(RADIO_MODULATION mod); + if ook: + m = ctypes.c_int(RADIO_MODULATION_OOK) + elif fsk: + m = ctypes.c_int(RADIO_MODULATION_FSK) + else: + raise RuntimeError("Must choose fsk or ook mode") + radio_modulation_fn(m) def transmitter(fsk=None, ook=None): """Change into transmitter mode""" - global mode - - trace("transmitter mode") - modulation(fsk, ook) - HRF_change_mode(MODE_TRANSMITER) - mode = "TRANSMITTER" - HRF_wait_txready() + #extern void radio_transmitter(RADIO_MODULATION mod); + if ook: + m = ctypes.c_int(RADIO_MODULATION_OOK) + elif fsk: + m = ctypes.c_int(RADIO_MODULATION_FSK) + else: # defaults to FSK + m = ctypes.c_int(RADIO_MODULATION_FSK) + radio_transmitter_fn(m) -def transmit(payload): +def transmit(payload, outer_times=1, inner_times=8, outer_delay=0): """Transmit a single payload using the present modulation scheme""" - spi.start_transaction() - if not modulation_fsk: - HRF_send_OOK_payload(payload) - else: - HRF_send_payload(payload) - spi.end_transaction() + #Note, this optionally does a mode change before and after + #extern void radio_transmit(uint8_t* payload, uint8_t len, uint8_t repeats); + + framelen = len(payload) + if framelen < 1 or framelen > 255: + raise ValueError("frame len must be 1..255") + if outer_times < 1: + raise ValueError("outer_times must be >0") + if inner_times < 1 or inner_times > 255: + raise ValueError("tx times must be 0..255") + + framelen = len(payload) + Frame = ctypes.c_ubyte * framelen + txframe = Frame(*payload) + inner_times = ctypes.c_ubyte(inner_times) + + for i in range(outer_times): + #TODO: transmit() will mode change if required + #this means that outer_times will keep popping and pushing the mode + #that might be ok, as it will force all the flags to clear? + radio_transmit_fn(txframe, framelen, inner_times) + if outer_delay != 0: + time.sleep(outer_delay) + + +def send_payload(payload, outer_times=1, inner_times=8, outer_delay=0): + """Transmit a payload in present modulation scheme, repeated""" + #Note, this does not do a mode change before or after, + #and assumes the mode is already transmit + #extern void radio_send_payload(uint8_t* payload, uint8_t len, uint8_t times); + + framelen = len(payload) + if framelen < 1 or framelen > 255: + raise ValueError("frame len must be 1..255") + if outer_times < 1: + raise ValueError("outer_times must be >0") + if inner_times < 1 or inner_times > 255: + raise ValueError("tx times must be 0..255") + Frame = ctypes.c_ubyte * framelen + txframe = Frame(*payload) + inner_times = ctypes.c_ubyte(inner_times) + + for i in range(outer_times): + radio_send_payload_fn(txframe, framelen, inner_times) + if outer_delay != 0: + time.sleep(outer_delay) def receiver(fsk=None, ook=None): """Change into receiver mode""" - global mode + #extern void radio_receiver(RADIO_MODULATION mod); + if ook: + m = ctypes.c_int(RADIO_MODULATION_OOK) + elif fsk: + m = ctypes.c_int(RADIO_MODULATION_FSK) + else: # defaults to FSK + m = ctypes.c_int(RADIO_MODULATION_FSK) - trace("receiver mode") - modulation(fsk, ook) - HRF_change_mode(MODE_RECEIVER) - HRF_wait_ready() - mode = "RECEIVER" + radio_receiver_fn(m) -def isReceiveWaiting(): +def is_receive_waiting(): """Check to see if a payload is waiting in the receive buffer""" - spi.start_transaction() - waiting = HRF_check_payload() - spi.end_transaction() - return waiting + #extern RADIO_RESULT radio_is_receive_waiting(void); + res = radio_is_receive_waiting_fn() + # this is RADIO_RESULT_OK_TRUE or RADIO_RESULT_OK_FALSE + # so it is safe to evaluate it as a boolean number. + return (res != 0) -def receive(): - """Receive a single payload from the buffer using the present modulation scheme""" - spi.start_transaction() - payload = HRF_receive_payload() - spi.end_transaction() - return payload +def receive(size=None): + """Receive a single payload""" + + if size == None: + return receive_cbp() + else: + return receive_len(size) + + +def receive_cbp(): + """Receive a count byte preceded payload""" + ##trace("receive_cbp") + + ##bufsize = MAX_RX_SIZE + bufsize = 255 # testing + Buffer = ctypes.c_ubyte * bufsize + rxbuf = Buffer() + buflen = ctypes.c_ubyte(bufsize) + #RADIO_RESULT radio_get_payload_cbp(uint8_t* buf, uint8_t buflen) + + result = radio_get_payload_cbp_fn(rxbuf, buflen) + + if result != 0: # RADIO_RESULT_OK + raise RuntimeError("Receive failed, radio.c error code %s" % hex(result)) + + size = 1+rxbuf[0] # The count byte in the payload + + # turn buffer into a list of bytes, using 'size' as the counter + rxlist = [] + for i in range(size): + rxlist.append(rxbuf[i]) + + ##trace("receive_cbp returhs %s" % tohex(rxlist)) + return rxlist # Python len(rxlist) tells us how many bytes including length byte if present + + +@untested +def receive_len(size): + """Receive a fixed payload size""" + + bufsize = size + + Buffer = ctypes.c_ubyte * bufsize + rxbuf = Buffer() + buflen = ctypes.c_ubyte(bufsize) + #RADIO_RESULT radio_get_payload_len(uint8_t* buf, uint8_t buflen) + + result = radio_get_payload_len_fn(rxbuf, buflen) + + if result != 0: # RADIO_RESULT_OK + raise RuntimeError("Receive failed, error code %s" % hex(result)) + + # turn buffer into a list of bytes, using 'size' as the counter + rxlist = [] + for i in range(size): + rxlist.append(rxbuf[i]) + + return rxlist # Python len(rxlist) tells us how many bytes including length byte if present + + +def standby(): + """Put radio into standby mode""" + #extern void radio_standby(void); + radio_standby_fn() def finished(): """Close the library down cleanly when finished""" - spi.finished() + #extern void radio_finished(void); + radio_finished_fn() # END diff --git a/src/energenie/radio2.py b/src/energenie/radio2.py deleted file mode 100644 index 7e2439c..0000000 --- a/src/energenie/radio2.py +++ /dev/null @@ -1,177 +0,0 @@ -# radio2.py 15/04/2015 D.J.Whale -# -# New version of the radio driver, with most of the fast stuff pushed into C. -# -# NOTE 1: This is partially tested, and only used for OOK transmit at the moment. -# FSK transmit and receive is inside radio.py, as the underlying radio.c code -# does not yet support FSK mode. - -# NOTE 2: Also there is an idea to do a python wrapper, build the C code -# for an Arduino and wrap it with a simple serial message handler. -# This would then make it possible to use the Energenie Radio on a Mac/PC/Linux -# machine but by still using the same higher level Python code. -# All you would need is a different radio.py that marshalled data to and from -# the Arduino via pyserial. - -#TODO: Should really add parameter validation here, so that C code doesn't have to. -#although it will be faster in C (C could be made optional, like an assert?) - -LIBNAME = "radio_rpi.so" -#LIBNAME = "drv/radio_mac.so" # testing - -import time -import ctypes -from os import path -mydir = path.dirname(path.abspath(__file__)) - -libradio = ctypes.cdll.LoadLibrary(mydir + "/" + LIBNAME) -radio_init_fn = libradio["radio_init"] -radio_reset_fn = libradio["radio_reset"] -radio_get_ver_fn = libradio["radio_get_ver"] -radio_modulation_fn = libradio["radio_modulation"] -radio_transmitter_fn = libradio["radio_transmitter"] -radio_transmit_fn = libradio["radio_transmit"] -radio_send_payload_fn = libradio["radio_send_payload"] -#radio_receiver_fn = libradio["radio_receiver"] -#radio_isReceiveWaiting_fn = libradio["radio_isReceiveWaiting"] -#radio_receive_fn = libradio["radio_receive"] -#radio_receive_payload_fn = libradio["radio_receive_payload"] -radio_standby_fn = libradio["radio_standby"] -radio_finished_fn = libradio["radio_finished"] - -RADIO_MODULATION_OOK = 0 -RADIO_MODULATION_FSK = 1 - - -def init(): - """Initialise the module ready for use""" - #extern void radio_init(void); - radio_init_fn() - - -def reset(): - """Reset the radio device""" - #extern void radio_reset(void); - radio_reset_fn() - - -def get_ver(): - """Read out the version number of the radio""" - return radio_get_ver_fn() - - -def modulation(fsk=None, ook=None): - """Switch modulation, if needed""" - #extern void radio_modulation(RADIO_MODULATION mod); - if ook: - m = ctypes.c_int(RADIO_MODULATION_OOK) - elif fsk: - m = ctypes.c_int(RADIO_MODULATION_FSK) - radio_modulation_fn(m) - - -def transmitter(fsk=None, ook=None): - """Change into transmitter mode""" - #extern void radio_transmitter(RADIO_MODULATION mod); - if ook: - m = ctypes.c_int(RADIO_MODULATION_OOK) - elif fsk: - m = ctypes.c_int(RADIO_MODULATION_FSK) - radio_transmitter_fn(m) - - -def transmit(payload, outer_times=1, inner_times=8, outer_delay=0): - """Transmit a single payload using the present modulation scheme""" - #Note, this optionally does a mode change before and after - #extern void radio_transmit(uint8_t* payload, uint8_t len, uint8_t repeats); - - framelen = len(payload) - if framelen < 1 or framelen > 255: - raise ValueError("frame len must be 1..255") - if outer_times < 1: - raise ValueError("outer_times must be >0") - if inner_times < 1 or inner_times > 255: - raise ValueError("tx times must be 0..255") - - framelen = len(payload) - Frame = ctypes.c_ubyte * framelen - txframe = Frame(*payload) - inner_times = ctypes.c_ubyte(inner_times) - - for i in range(outer_times): - radio_transmit_fn(txframe, framelen, inner_times) - if outer_delay != 0: - time.sleep(outer_delay) - - -def send_payload(payload, outer_times=1, inner_times=8, outer_delay=0): - """Transmit a payload in present modulation scheme, repeated""" - #Note, this does not do a mode change before or after, - #and assumes the mode is already transmit - #extern void radio_send_payload(uint8_t* payload, uint8_t len, uint8_t times); - - framelen = len(payload) - if framelen < 1 or framelen > 255: - raise ValueError("frame len must be 1..255") - if outer_times < 1: - raise ValueError("outer_times must be >0") - if inner_times < 1 or inner_times > 255: - raise ValueError("tx times must be 0..255") - Frame = ctypes.c_ubyte * framelen - txframe = Frame(*payload) - inner_times = ctypes.c_ubyte(inner_times) - - for i in range(outer_times): - radio_send_payload_fn(txframe, framelen, inner_times) - if outer_delay != 0: - time.sleep(outer_delay) - - -#def receiver(fsk=None, ook=None): -# """Change into receiver mode""" -# #extern void radio_receiver(RADIO_MODULATION mod); -# if ook: -# m = ctypes.c_int(RADIO_MODULATION_OOK) -# elif fsk: -# m = ctypes.c_int(RADIO_MODULATION_FSK) -# -# radio_receiver_fn(m) - - -#def isReceiveWaiting(): -# """Check to see if a payload is waiting in the receive buffer""" -# #extern RADIO_RESULT radio_isReceiveWaiting(void); -# pass # TODO -# # returns bool -# ##res = radio_isReceiveWaitingFn() - - -#def receive(): -# """Put radio into receive mode and receive""" -# #extern RADIO_RESULT radio_receive(uint8_t* buf, uint8_t len) -# pass # TODO -# ##radio_receive_fn(buf, len) -# # returns list of bytes - - -#def radio_receive_payload(): -# """Receive a single payload""" -# #extern RADIO_RESULT radio_receive_payload(uint8_t* buf, uint8_t len); -# pass # TODO -# ##radio_receive_payload_fn(buf, len) -# # returns list of bytes - - -def standby(): - """Put radio into standby mode""" - #extern void radio_standby(void); - radio_standby_fn() - - -def finished(): - """Close the library down cleanly when finished""" - #extern void radio_finished(void); - radio_finished_fn() - - -# END diff --git a/src/energenie/radio2_test.py b/src/energenie/radio2_test.py deleted file mode 100644 index 1a13ceb..0000000 --- a/src/energenie/radio2_test.py +++ /dev/null @@ -1,71 +0,0 @@ -# radio2_test.py 15/04/2016 D.J.Whale -# -# A simple Energenie radio exerciser -# -# Repeatedly transmits OOK packets to turn switch 1 on and off. -# - -import radio2 as radio -import time - -# How many times to repeat the OOK payload. -# 4800bps*8*16=26ms per payload -# 75 payloads is 2 seconds -# 255 payloads is 6.8 seconds -TIMES = 75 -DELAY = 0.5 - -# The 'radio' module knows nothing about the Energenie (HS1527) bit encoding, -# so this test code manually encodes the bits. -# For the full Python stack, there is an encoder module that can generate -# specific payloads. Repeats are done in radio_transmitter. -# The HRF preamble feature is no longer used, it's more predictable to -# put the preamble in the payload. - - -# preamble pulse with timing violation gap -PREAMBLE = [0x80, 0x00, 0x00, 0x00] - -# Energenie 'random' 20 bit address is 0x6C6C6 -# 0110 1100 0110 1100 0110 -# 0 encoded as 8 (1000) -# 1 encoded as E (1110) -ADDR = [0x8E, 0xE8, 0xEE, 0x88, 0x8E, 0xE8, 0xEE, 0x88, 0x8E, 0xE8] - -# Energenie 'switch 1 ON' command F 1111 (0xEE, 0xEE) -SW1_ON = [0xEE, 0xEE] - -# Energenie 'switch 1 OFF' command E 1110 (0xEE, 0xE8) -SW1_OFF = [0xEE, 0xE8] - -# manual preamble, 20 bit encoded address, 4 encoded data bits -enc_1on = PREAMBLE + ADDR + SW1_ON -enc_1off = PREAMBLE + ADDR + SW1_OFF - - -def radio_test_ook(): - """Repeatedly test switch 1 ON then OFF""" - - radio.init() - # init() defaults to standby() - try: - radio.modulation(ook=True) - while True: - print("Switch 1 ON") - radio.transmit(enc_1on, TIMES) - # auto returns to standby - if DELAY!=0: time.sleep(DELAY) - - print("Switch 1 OFF") - radio.transmit(enc_1off, TIMES) - # auto returns to standby - if DELAY!=0: time.sleep(DELAY) - - finally: - radio.finished() - - -if __name__ == "__main__": - radio_test_ook() - -# END diff --git a/src/energenie/radio_rpi.so b/src/energenie/radio_rpi.so deleted file mode 100755 index 0129587..0000000 --- a/src/energenie/radio_rpi.so +++ /dev/null Binary files differ diff --git a/src/energenie/radio_test.py b/src/energenie/radio_test.py new file mode 100644 index 0000000..b02f117 --- /dev/null +++ b/src/energenie/radio_test.py @@ -0,0 +1,71 @@ +# radio_test.py 15/04/2016 D.J.Whale +# +# A simple Energenie radio exerciser +# +# Repeatedly transmits OOK packets to turn switch 1 on and off. +# + +import radio +import time + +# How many times to repeat the OOK payload. +# 4800bps*8*16=26ms per payload +# 75 payloads is 2 seconds +# 255 payloads is 6.8 seconds +TIMES = 75 +DELAY = 0.5 + +# The 'radio' module knows nothing about the Energenie (HS1527) bit encoding, +# so this test code manually encodes the bits. +# For the full Python stack, there is an encoder module that can generate +# specific payloads. Repeats are done in radio_transmitter. +# The HRF preamble feature is no longer used, it's more predictable to +# put the preamble in the payload. + + +# preamble pulse with timing violation gap +PREAMBLE = [0x80, 0x00, 0x00, 0x00] + +# Energenie 'random' 20 bit address is 0x6C6C6 +# 0110 1100 0110 1100 0110 +# 0 encoded as 8 (1000) +# 1 encoded as E (1110) +ADDR = [0x8E, 0xE8, 0xEE, 0x88, 0x8E, 0xE8, 0xEE, 0x88, 0x8E, 0xE8] + +# Energenie 'switch 1 ON' command F 1111 (0xEE, 0xEE) +SW1_ON = [0xEE, 0xEE] + +# Energenie 'switch 1 OFF' command E 1110 (0xEE, 0xE8) +SW1_OFF = [0xEE, 0xE8] + +# manual preamble, 20 bit encoded address, 4 encoded data bits +enc_1on = PREAMBLE + ADDR + SW1_ON +enc_1off = PREAMBLE + ADDR + SW1_OFF + + +def radio_test_ook(): + """Repeatedly test switch 1 ON then OFF""" + + radio.init() + # init() defaults to standby() + try: + radio.modulation(ook=True) + while True: + print("Switch 1 ON") + radio.transmit(enc_1on, TIMES) + # auto returns to standby + if DELAY!=0: time.sleep(DELAY) + + print("Switch 1 OFF") + radio.transmit(enc_1off, TIMES) + # auto returns to standby + if DELAY!=0: time.sleep(DELAY) + + finally: + radio.finished() + + +if __name__ == "__main__": + radio_test_ook() + +# END diff --git a/src/energenie/spi.py b/src/energenie/spi.py deleted file mode 100644 index 74f3a10..0000000 --- a/src/energenie/spi.py +++ /dev/null @@ -1,140 +0,0 @@ -# spi.py 19/07/2014 D.J.Whale -# -# a C based SPI driver, with a python wrapper - -LIBNAME = "spi_rpi.so" - -import ctypes -import time - -from os import path -mydir = path.dirname(path.abspath(__file__)) - -RESET = 25 # BCM GPIO -LED_GREEN = 27 # BCM GPIO (not B rev1) -LED_RED = 22 # BCM GPIO - -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"] - -# Might put this in a separate rpi_gpio.so and dynamically link both -# from rpi_spi and from python, but have to be careful with sharing -# memmap. For now, we only use this to control the radio RESET, -# so let's leave this embedded inside the rpi_spi.so until we -# need any more visibility -#gpio_init_fn = libspi["gpio_init"] -#gpio_setin_fn = libspi["gpio_setin"] -gpio_setout_fn = libspi["gpio_setout"] -gpio_high_fn = libspi["gpio_high"] -gpio_low_fn = libspi["gpio_low"] -#gpio_write_fn = libspi["gpio_write"] -#gpio_read_fn = libspi["gpio_read"] - - -def trace(msg): - pass #print("spi:" + msg) - - -# Note, this is radio specific, not SPI specific. hmm. -# fine for now to test the theory, but architecturally this is wrong. -# might have an optional reset line in spi_config, and an spi_reset() -# in spi.c that excercises it if required, as a more generic way -# to do this, as most SPI devices have a reset line. If value not -# defined, nothing happens. If value defined, it must have a -# polarity too, and spi.c will set it to an output inactive -# until reset is required. Also need to set up a reset active -# time and a 'after reset' guard time. That will cover most -# cases generically without having to surface the whole inner GPIO -# out to the python. - -def reset(): - trace("reset") - - reset = ctypes.c_int(RESET) - gpio_setout_fn(reset) - gpio_high_fn(reset) - time.sleep(0.1) - gpio_low_fn(reset) - time.sleep(0.1) - - # Put LEDs into known off state - led_red = ctypes.c_int(LED_RED) - led_green = ctypes.c_int(LED_GREEN) - gpio_setout_fn(led_red) - gpio_low_fn(led_red) - gpio_setout_fn(led_green) - gpio_low_fn(led_green) - - -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 start_transaction(): - """Start a transmit or receive, perhaps multiple bursts""" - # turn the GREEN LED on - led_green = ctypes.c_int(LED_GREEN) - gpio_high_fn(led_green) - - -def end_transaction(): - """End a transmit or receive, perhaps multiple listens""" - # turn the GREEN LED off - led_green = ctypes.c_int(LED_GREEN) - gpio_low_fn(led_green) - - -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() - - -# END - - diff --git a/src/energenie/spi_rpi.so b/src/energenie/spi_rpi.so deleted file mode 100755 index 6f81455..0000000 --- a/src/energenie/spi_rpi.so +++ /dev/null Binary files differ diff --git a/src/legacy.py b/src/legacy.py index 0e7b7b2..a665bd7 100644 --- a/src/legacy.py +++ b/src/legacy.py @@ -8,9 +8,7 @@ import time -from energenie import encoder -# moving over to the new, faster, C radio driver -from energenie import radio2 as radio +from energenie import encoder, radio # How many times to send messages in the driver fast loop # Present version of driver limits to 15 @@ -34,7 +32,7 @@ # Prebuild all possible message up front, to make switching code faster HOUSE_ADDRESS = None # Use default energenie quasi-random address 0x6C6C6 -#HOUSE_ADDRESS = 0xA0170 # Captured address of David's RF hand controller +##HOUSE_ADDRESS = 0xA0170 # Captured address of David's RF hand controller ALL_ON = encoder.build_switch_msg(True, house_address=HOUSE_ADDRESS) ONE_ON = encoder.build_switch_msg(True, device_address=1, house_address=HOUSE_ADDRESS) @@ -138,13 +136,12 @@ radio.modulation(ook=True) try: - #pattern_test() + ##pattern_test() legacy_learn_mode() legacy_switch_loop() - #switch1_loop() + ##switch1_loop() finally: radio.finished() - # END diff --git a/src/monitor.py b/src/monitor.py index 43ed0ee..9dbf3ce 100644 --- a/src/monitor.py +++ b/src/monitor.py @@ -9,133 +9,20 @@ # However, it will log all messages from MiHome monitor, adaptor plus and house monitor # to a CSV log file, so could be the basis for a non-controlling energy logging app. -import time +from energenie import Registry, Devices, Messages, OpenThings, radio -from energenie import OpenThings -from energenie import Devices, Messages, radio -import os - -LOG_FILENAME = "energenie.csv" +import Logger def warning(msg): print("warning:%s" % str(msg)) + def trace(msg): print("monitor:%s" % str(msg)) -log_file = None - -def logMessage(msg): - HEADINGS = 'timestamp,mfrid,prodid,sensorid,flags,switch,voltage,freq,reactive,real' - - global log_file - if log_file == None: - if not os.path.isfile(LOG_FILENAME): - log_file = open(LOG_FILENAME, 'w') - log_file.write(HEADINGS + '\n') - else: - log_file = open(LOG_FILENAME, 'a') # append - - # get the header - header = msg['header'] - timestamp = time.time() - mfrid = header['mfrid'] - productid = header['productid'] - sensorid = header['sensorid'] - - # set defaults for any data that doesn't appear in this message - # but build flags so we know which ones this contains - flags = [0 for i in range(7)] - switch = None - voltage = None - freq = None - reactive = None - real = None - apparent = None - current = None - - # capture any data that we want - #print(msg) - for rec in msg['recs']: - paramid = rec['paramid'] - try: - value = rec['value'] - except: - value = None - - if paramid == OpenThings.PARAM_SWITCH_STATE: - switch = value - flags[0] = 1 - elif paramid == OpenThings.PARAM_VOLTAGE: - flags[1] = 1 - voltage = value - elif paramid == OpenThings.PARAM_FREQUENCY: - flags[2] = 1 - freq = value - elif paramid == OpenThings.PARAM_REACTIVE_POWER: - flags[3] = 1 - reactive = value - elif paramid == OpenThings.PARAM_REAL_POWER: - flags[4] = 1 - real = value - elif paramid == OpenThings.PARAM_APPARENT_POWER: - flags[5] = 1 - apparent = value - elif paramid == OpenThings.PARAM_CURRENT: - flags[6] = 1 - current = value - - # generate a line of CSV - flags = "".join([str(a) for a in flags]) - csv = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" % (timestamp, mfrid, productid, sensorid, flags, switch, voltage, freq, reactive, real, apparent, current) - log_file.write(csv + '\n') - log_file.flush() - trace(csv) # testing - #----- TEST APPLICATION ------------------------------------------------------- -directory = {} - -def allkeys(d): - result = "" - for k in d: - if len(result) != 0: - result += ',' - result += str(k) - return result - - -def updateDirectory(message): - """Update the local directory with information about this device""" - now = time.time() - header = message["header"] - sensorId = header["sensorid"] - - if not directory.has_key(sensorId): - # new device discovered - desc = Devices.getDescription(header["mfrid"], header["productid"]) - print("ADD device:%s %s" % (hex(sensorId), desc)) - directory[sensorId] = {"header": message["header"]} - #trace(allkeys(directory)) - - directory[sensorId]["time"] = now - #TODO would be good to keep recs, but need to iterate through all and key by paramid, - #not as a list index, else merging will be hard. - - -def send_join_ack(mfrid, productid, sensorid): - # send back a JOIN ACK, so that join light stops flashing - response = OpenThings.alterMessage(Messages.JOIN_ACK, - header_mfrid=mfrid, - header_productid=productid, - header_sensorid=sensorid) - p = OpenThings.encode(response) - radio.transmitter() - radio.transmit(p) - radio.receiver() - - def monitor_loop(): """Capture any incoming messages and log to CSV file""" @@ -143,8 +30,8 @@ while True: # See if there is a payload, and if there is, process it - if radio.isReceiveWaiting(): - #trace("receiving payload") + if radio.is_receive_waiting(): + trace("receiving payload") payload = radio.receive() try: decoded = OpenThings.decode(payload) @@ -154,9 +41,9 @@ OpenThings.showMessage(decoded) # Any device that reports will be added to the non-persistent directory - updateDirectory(decoded) - #trace(decoded) - logMessage(decoded) + Registry.update(decoded) + ##trace(decoded) + Logger.logMessage(decoded) # Process any JOIN messages by sending back a JOIN-ACK to turn the LED off if len(decoded["recs"]) == 0: @@ -164,14 +51,11 @@ print("Empty record:%s" % decoded) else: # assume only 1 rec in a join, for now - #TODO: use OpenThings.getFromMessage("header_mfrid") if decoded["recs"][0]["paramid"] == OpenThings.PARAM_JOIN: - header = decoded["header"] - mfrid = header["mfrid"] - productid = header["productid"] - sensorid = header["sensorid"] - send_join_ack(mfrid, productid, sensorid) - + mfrid = OpenThings.getFromMessage(decoded, "header_mfrid") + productid = OpenThings.getFromMessage(decoded, "header_productid") + sensorid = OpenThings.getFromMessage(decoded, "header_sensorid") + Messages.send_join_ack(radio, mfrid, productid, sensorid) if __name__ == "__main__": diff --git a/src/switch.py b/src/switch.py index f283936..2c6c2ef 100644 --- a/src/switch.py +++ b/src/switch.py @@ -10,130 +10,83 @@ # Don't expect this to be a good starting point for an application. # Consider waiting for me to finish developing the device object interface first. -import time - -from energenie import OpenThings -from energenie import Devices, Messages, radio +from energenie import Devices, Messages, Registry, OpenThings, radio from Timer import Timer + # Increase this if you have lots of switches, so that the receiver has enough # time to receive update messages, otherwise your devices won't make it into # the device directory. -TX_RATE = 10 # seconds between each switch change cycle +TX_RATE = 2 # seconds between each switch change cycle + def warning(msg): print("warning:%s" % str(msg)) + def trace(msg): print("monitor:%s" % str(msg)) #----- TEST APPLICATION ------------------------------------------------------- -directory = {} +def switch_sniff_loop(): + """Listen to sensor messages and add them to the Registry""" -def allkeys(d): - result = "" - for k in d: - if len(result) != 0: - result += ',' - result += str(k) - return result + # See if there is a payload, and if there is, process it + if radio.is_receive_waiting(): + ##trace("receiving payload") + payload = radio.receive() + try: + decoded = OpenThings.decode(payload) + except OpenThings.OpenThingsException as e: + warning("Can't decode payload:" + str(e)) + return - -def updateDirectory(message): - """Update the local directory with information about this device""" - now = time.time() - header = message["header"] - sensorId = header["sensorid"] + OpenThings.showMessage(decoded) + # Any device that reports will be added to the non-persistent directory + Registry.update(decoded) + ##trace(decoded) - if not directory.has_key(sensorId): - # new device discovered - desc = Devices.getDescription(header["mfrid"], header["productid"]) - print("ADD device:%s %s" % (hex(sensorId), desc)) - directory[sensorId] = {"header": message["header"]} - #trace(allkeys(directory)) - - directory[sensorId]["time"] = now - #TODO would be good to keep recs, but need to iterate through all and key by paramid, - #not as a list index, else merging will be hard. + # Process any JOIN messages by sending back a JOIN-ACK to turn the LED off + if len(decoded["recs"]) == 0: + # handle messages with zero recs in them silently + print("Empty record:%s" % decoded) + else: + # assume only 1 rec in a join, for now + if decoded["recs"][0]["paramid"] == OpenThings.PARAM_JOIN: + mfrid = OpenThings.getFromMessage(decoded, "header_mfrid") + productid = OpenThings.getFromMessage(decoded, "header_productid") + sensorid = OpenThings.getFromMessage(decoded, "header_sensorid") + Messages.send_join_ack(radio, mfrid, productid, sensorid) -def send_join_ack(mfrid, productid, sensorid): - # send back a JOIN ACK, so that join light stops flashing - response = OpenThings.alterMessage(Messages.JOIN_ACK, - header_mfrid=mfrid, - header_productid=productid, - header_sensorid=sensorid) - p = OpenThings.encode(response) - radio.transmitter() - radio.transmit(p) - radio.receiver() +def switch_toggle_loop(): + """Toggle the switch on all devices in the directory""" + global switch_state -def switch_loop(): - """Listen to sensor messages, and turn switches on and off every few seconds""" + if Registry.size() > 0 and sendSwitchTimer.check(): + print("transmit") + radio.transmitter() - # Define the schedule of message polling - sendSwitchTimer = Timer(TX_RATE, 1) # every n seconds offset by initial 1 - switch_state = 0 # OFF - radio.receiver() + for sensorid in Registry.get_sensorids(): + # Only try to toggle the switch for devices that actually have a switch + header = Registry.get_info(sensorid)["header"] + mfrid = header["mfrid"] + productid = header["productid"] - while True: - # See if there is a payload, and if there is, process it - if radio.isReceiveWaiting(): - #trace("receiving payload") - payload = radio.receive() - try: - decoded = OpenThings.decode(payload) - except OpenThings.OpenThingsException as e: - warning("Can't decode payload:" + str(e)) - continue - - OpenThings.showMessage(decoded) - # Any device that reports will be added to the non-persistent directory - updateDirectory(decoded) - #trace(decoded) + if Devices.hasSwitch(mfrid, productid): + request = OpenThings.alterMessage(Messages.SWITCH, + header_sensorid=sensorid, + recs_0_value=switch_state) + p = OpenThings.encode(request) + print("Sending switch message to %s %s" % (hex(productid), hex(sensorid))) + # Transmit multiple times, hope one of them gets through + radio.transmit(p, inner_times=2) - # Process any JOIN messages by sending back a JOIN-ACK to turn the LED off - if len(decoded["recs"]) == 0: - # handle messages with zero recs in them silently - print("Empty record:%s" % decoded) - else: - # assume only 1 rec in a join, for now - #TODO: use OpenThings.getFromMessage("header_mfrid") - if decoded["recs"][0]["paramid"] == OpenThings.PARAM_JOIN: - header = decoded["header"] - mfrid = header["mfrid"] - productid = header["productid"] - sensorid = header["sensorid"] - send_join_ack(mfrid, productid, sensorid) - - - # Toggle the switch on all devices in the directory - if len(directory) > 0 and sendSwitchTimer.check(): - print("transmit") - radio.transmitter() - - for sensorid in directory.keys(): - # Only try to toggle the switch for devices that actually have a switch - header = directory[sensorid]["header"] - mfrid = header["mfrid"] - productid = header["productid"] - - if Devices.hasSwitch(mfrid, productid): - request = OpenThings.alterMessage(Messages.SWITCH, - header_sensorid=sensorid, - recs_0_value=switch_state) - p = OpenThings.encode(request) - print("Sending switch message to %s %s" % (hex(productid), hex(sensorid))) - # Transmit multiple times, hope one of them gets through - for i in range(4): - radio.transmit(p) - - radio.receiver() - print("receive") - switch_state = (switch_state+1) % 2 # toggle + radio.receiver() + switch_state = (switch_state+1) % 2 # toggle if __name__ == "__main__": @@ -142,8 +95,23 @@ radio.init() OpenThings.init(Devices.CRYPT_PID) + # Seed the registry with a known device, to simplify tx-only testing + SENSOR_ID = 0x68B # captured from a real device + device_header = OpenThings.alterMessage(Messages.REGISTERED_SENSOR, + header_mfrid = Devices.MFRID, + header_productid = Devices.PRODUCTID_MIHO005, # adaptor plus + header_sensorid = SENSOR_ID) + Registry.update(device_header) + + + sendSwitchTimer = Timer(TX_RATE, 1) # every n seconds offset by initial 1 + switch_state = 0 # OFF + radio.receiver() + try: - switch_loop() + while True: + switch_sniff_loop() + switch_toggle_loop() finally: radio.finished()