Example #1
0
def start():
    print('Started')
    lora = LoRa(mode=LoRa.LORAWAN,
                region=LoRa.AS923,
                device_class=LoRa.CLASS_C,
                adr=False)

    lora_otaa_join(lora)
    # create a LoRa socket
    lora_socket = socket.socket(socket.AF_LORA, socket.SOCK_RAW)

    # set the LoRaWAN data rate
    # self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_DR, self.config["lora"]["data_rate"])
    #lora_socket.setsockopt(socket.SOL_LORA, socket.SO_DR, 2)

    # msg are confirmed at the FMS level
    lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, 0)

    # make the socket non blocking y default
    lora_socket.setblocking(False)

    #prepare_channels(lora, 2, 2) #How do I use this??

    lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT
                           | LoRa.TX_FAILED_EVENT),
                  handler=lora_cb)
Example #2
0
def create_lora_abp(tx_power = 14, coding_rate = 1):
    assert 2 <= tx_power <= 14
    assert 1 <= coding_rate <= 4

    lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868, adr = False)
    lora.tx_power(tx_power)    # from 2 to 14
    lora.coding_rate(coding_rate) # 1 = 4/5, 2 = 4/6, 3 = 4/7, 4 = 4/8
    lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=update_tx_params_cb)
    lora.join(activation=LoRa.ABP, auth=(dev_addr, nwk_swkey, app_swkey))
    return lora
Example #3
0
def create_lora_adr(handler, app_eui, app_key):
    lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868, adr = False)
    lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=handler)
    lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0, dr=0)

    while not lora.has_joined():
        time.sleep(2.5)
        print("Waiting for lora to join")

    return lora
Example #4
0
def create_lora(handler, app_eui, app_key, tx_power = 14, coding_rate = 1):
    assert 2 <= tx_power <= 14
    assert 1 <= coding_rate <= 4

    lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868, adr = False)
    lora.tx_power(tx_power)    # from 2 to 14
    lora.coding_rate(coding_rate) # 1 = 4/5, 2 = 4/6, 3 = 4/7, 4 = 4/8
    lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=handler)
    lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0, dr=0)

    while not lora.has_joined():
        time.sleep(2.5)
        print("Waiting for lora to join")

    return lora
Example #5
0
def start():
    print('Started')
    lora = LoRa(mode=LoRa.LORAWAN,
                region=LoRa.AS923,
                device_class=LoRa.CLASS_C,
                adr=False)

    lora_otaa_join(lora)
    # create a LoRa socket
    lora_socket = socket.socket(socket.AF_LORA, socket.SOCK_RAW)

    # set the LoRaWAN data rate
    # self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_DR, self.config["lora"]["data_rate"])
    #lora_socket.setsockopt(socket.SOL_LORA, socket.SO_DR, 2)

    # msg are confirmed at the FMS level
    lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, 0)

    # make the socket non blocking y default
    lora_socket.setblocking(False)

    #prepare_channels(lora, 2, 2)

    #for channel in range(0, 15):
    #    lora.remove_channel(channel)

    lora.add_channel(0, frequency=923200000, dr_min=0, dr_max=6)
    lora.add_channel(1, frequency=923400000, dr_min=0, dr_max=6)
    #lora.add_channel(2, frequency=922200000, dr_min=0, dr_max=6)
    #lora.add_channel(3, frequency=922400000, dr_min=0, dr_max=6)
    #lora.add_channel(4, frequency=922600000, dr_min=0, dr_max=6)
    #lora.add_channel(5, frequency=922800000, dr_min=0, dr_max=6)
    #lora.add_channel(6, frequency=922000000, dr_min=0, dr_max=6)

    lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT
                           | LoRa.TX_FAILED_EVENT),
                  handler=lora_cb)

    lora_socket.send(bytes([0x01, 0x02, 0x03]))

    while (True):
        time.sleep(5)
        print(".")
Example #6
0
    print('  event: ', event)
    if event & LoRa.RX_PACKET_EVENT:
        print('lora packed received')
        recv_ack = lora_sock.recv(256)
        if len(recv_ack) > 0:
            device_id, pkg_len, ack = struct.unpack(_LORA_PKG_ACK_FORMAT, recv_ack)
            if device_id == DEVICE_ID:
                if ack == 200:
                    print("ACK")
                else:
                    print("Message Failed", ack)
    elif event & LoRa.TX_PACKET_EVENT:
        print('lora packet sent - we should never see this message')


lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=handle_ack)
print('receive handler connected')


while True:
    # Package send containing a simple string
    _input = input('color?')
    values = _input.split(' ')
    msg = values[0]
    if len(values) > 1:
        try:
            DEVICE_ID = int(values[1])
            print('changing DEVICE_ID to', DEVICE_ID)
        except:
            pass
    print('couleur: (%d) %r' % (len(msg), msg))
Example #7
0
        received_object = cbor.loads(received)
        led_color_value = received_object.get('led-color', None)
        led_value = received_object.get('led', None)

        if led_color_value:
            pycom.rgbled(int(led_color_value))

        if led_value:
            if led_value == False:
                pycom.rgbled(0x000000)

    elif events & LoRa.TX_PACKET_EVENT:
        print('LoRa packet sent')


lora.callback(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT, lora_callback)

print('creating the i2c component')

pwm = PWM(0, frequency=5000)  # use PWM timer 0, with a frequency of 5KHz
# create pwm channel on pin P12 with a duty cycle of 50%
pwm_c = pwm.channel(0, pin='P22', duty_cycle=1.0)

i2c = I2C(0, I2C.MASTER)  # create and init as a master
sensor0 = drivers.adafruit_sht31d.SHT31D(
    i2c, address=drivers.adafruit_sht31d._SHT31_ADDRESSES[0])
#sensor1 = drivers.adafruit_sht31d.SHT31D(i2c, address=drivers.adafruit_sht31d._SHT31_ADDRESSES[1])
print('i2c component created')
while True:
    print('i2c component reading')
    t0 = sensor0.temperature
Example #8
0
	
# Init LoRa and socket	
lora = LoRa(mode=LoRa.LORA, tx_power = 2, sf = 7, bandwidth=LoRa.BW_500KHZ, coding_rate=LoRa.CODING_4_8, preamble=8, frequency=868000000)
s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
s.setblocking(False)

# Mount SD card and open a file on it
time.sleep(2)
sd = SD()
os.mount(sd, '/sd')
f = open('/sd/gps-record.txt', 'w')
f.write('LoRa test with: tx_power = 2, sf = 7, bandwidth=LoRa.BW_500KHZ, coding_rate=LoRa.CODING_4_8, preamble=8, frequency=868000000\n')

# Send first packet to init comm on the other side
s.send('Ping')
print('Send Ping')

# Init RGB Led and global variables
pycom.heartbeat(False)
pycom.rgbled(0x7f0000)
toggle = True
fileOpen = True

# Init button for closing file purpose
p_in = Pin('P10', mode=Pin.IN, pull = Pin.PULL_UP)

# Call lora_cb function when a packet is received
lora.callback(trigger = LoRa.RX_PACKET_EVENT,  handler = lora_cb)

# Call pin_handler function when button is pressed
p_in.callback(Pin.IRQ_FALLING, pin_handler)
def main():

    pycom.heartbeat(False)

    # Sensor initializations with default params
    # bus   = 1
    # sda   = P10
    # scl   = P11
    # baud  = 20000
    # interval  = 10
    pm_sensor = sps30() if using_pm else None
    co2_sensor = scd30() if using_co2 else None

    # Start sensors in a separate thread
    pm_thread = _thread.start_new_thread(pm_sensor.start,
                                         ()) if using_pm else None
    co2_thread = _thread.start_new_thread(co2_sensor.start,
                                          ()) if using_co2 else None

    # Prepare LoRa channels
    lora = LoRa(mode=LoRa.LORAWAN,
                region=LoRa.US915,
                device_class=LoRa.CLASS_C)
    prepare_channels(lora, LORA_FREQUENCY)
    #lora = LoRa(mode=LoRa.LORA, region=LoRa.US915, frequency=904600000, bandwidth=LoRa.BW_500KHZ, sf=8)
    # Join LoRa network with OTAA
    lora.join(activation=LoRa.OTAA,
              auth=(dev_eui, app_key, nwk_key),
              timeout=0,
              dr=0)
    # wait until the module has joined the network
    print('Over the air network activation ... ', end='')
    while not lora.has_joined():
        time.sleep(2.5)
        print('.', end='')
    print('')

    # Socket initializations
    lora_socket = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
    lora_socket.setsockopt(socket.SOL_LORA, socket.SO_DR, LORA_DR)
    # msg are confirmed at the FMS level
    lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, False)
    # make the socket non blocking by default
    lora_socket.setblocking(False)

    lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT
                           | LoRa.TX_FAILED_EVENT),
                  handler=lora_cb)

    time.sleep(4)  # this timer is important and caused me some trouble ...

    # Send pm (payload=40bytes) and co2 (payload=12bytes) data every 5 minutes
    while True:
        # Poll data
        if using_pm:
            pm_data = pm_sensor.get_packed_msg()
        if using_co2:
            co2_data = co2_sensor.get_packed_msg()
        if using_pysense_sensor:
            py = Pysense()
            mp = MPL3115A2(
                py, mode=ALTITUDE
            )  # Returns height in meters. Mode may also be set to PRESSURE, returning a value in Pascals
            si = SI7006A20(py)
            lt = LTR329ALS01(py)
            li = LIS2HH12(py)

        # Send data
        if using_pm:
            send_pkt(lora_socket, pm_data, 8)
        if using_co2:
            send_pkt(lora_socket, co2_data, 9)
        if using_pysense_sensor:

            temp = si.temperature()
            rh = si.humidity()
            rlux = lt.light()[0]
            blux = lt.light()[1]

            pysense_pkt = struct.pack('<ffii', temp, rh, rlux, blux)
            send_pkt(lora_socket, pysense_pkt, 10)

        time.sleep(15 - using_pm * 5 - using_co2 * 5 -
                   using_pysense_sensor * 5)

    # Stop polling and end threads
    if using_pm:
        pm_sensor.stop()
    if using_co2:
        co2_sensor.stop()
app_key = binascii.unhexlify('')  # removed before uploading it to github

g_tx_power = 14
g_data_rate = 0  # sf = 12
g_coding_rate = 1
g_lambda_ = 1
g_pkt_length = 10
paq_bytes = None
g_counter = -1
g_keep_alive_counter = 0

lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868, adr=False)
lora.nvram_restore()
if lora.has_joined():
    print('Joining not required')
    lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=packet_received_hd)
    lora.nvram_save()
else:
    print('Joining for the first time')
    lora = create_lora_adr(handler=packet_received_hd,
                           app_eui=app_eui,
                           app_key=app_key)
    lora.nvram_save()

s = create_socket_adr()

while True:
    set_tx_power(lora, tx_power=14)
    s.send(bytes([0x01, 0x02, 0x03, 0x04, 0x05]))
    time.sleep(3)
Example #11
0
class NanoGateway:
    """
    Nano gateway class, set up by default for use with TTN, but can be configured
    for any other network supporting the Semtech Packet Forwarder.
    Only required configuration is wifi_ssid and wifi_password which are used for
    connecting to the Internet.
    """

    def __init__(self, id, frequency, datarate, ssid, password, server, port, ntp_server='pool.ntp.org', ntp_period=3600):
        self.id = id
        self.server = server
        self.port = port

        self.frequency = frequency
        self.datarate = datarate

        self.ssid = ssid
        self.password = password

        self.ntp_server = ntp_server
        self.ntp_period = ntp_period

        self.server_ip = None

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.sf = self._dr_to_sf(self.datarate)
        self.bw = self._dr_to_bw(self.datarate)
        self.region = LoRa.AU915

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.wlan = None
        self.sock = None
        self.udp_stop = False
        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

        self.rtc = machine.RTC()

    def start(self):
        """
        Starts the LoRaWAN nano gateway.
        """

        self._log('Starting LoRaWAN nano gateway with id: {}', self.id)

        # setup WiFi as a station and connect
        self.wlan = WLAN(mode=WLAN.STA)
        self._connect_to_wifi()

        # get a time sync
        self._log('Syncing time with {} ...', self.ntp_server)
        self.rtc.ntp_sync(self.ntp_server, update_period=self.ntp_period)
        while not self.rtc.synced():
            utime.sleep_ms(50)
        self._log("RTC NTP sync complete")

        # get the server IP and create an UDP socket
        self.server_ip = usocket.getaddrinfo(self.server, self.port)[0][-1]
        self._log('Opening UDP socket to {} ({}) port {}...', self.server, self.server_ip[0], self.server_ip[1])
        self.sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM, usocket.IPPROTO_UDP)
        self.sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # push the first time immediatelly
        self._push_data(self._make_stat_packet())

        # create the alarms
        self.stat_alarm = Timer.Alarm(handler=lambda t: self._push_data(self._make_stat_packet()), s=60, periodic=True)
        self.pull_alarm = Timer.Alarm(handler=lambda u: self._pull_data(), s=25, periodic=True)

        # start the UDP receive thread
        self.udp_stop = False
        _thread.start_new_thread(self._udp_thread, ())

        # initialize the LoRa radio in LORA mode
        self._log('Setting up the LoRa radio at {} Mhz using {}', self._freq_to_float(self.frequency), self.datarate)
        self.lora = LoRa(
            mode=LoRa.LORA,
            region=self.region,
            frequency=self.frequency,
            bandwidth=self.bw,
            sf=self.sf,
            preamble=8,
            coding_rate=LoRa.CODING_4_5,
            power_mode=LoRa.ALWAYS_ON,
            #tx_iq=True
        )

        # create a raw LoRa socket
        self.lora_sock = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW)
        self.lora_sock.setblocking(False)
        self.lora_tx_done = False

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT), handler=self._lora_cb)
        
        if uos.uname()[0] == "LoPy":
            self.window_compensation = -1000
        else:
            self.window_compensation = -6000
        self.downlink_count = 0
        
        self._log('LoRaWAN nano gateway online')

    def stop(self):
        """
        Stops the LoRaWAN nano gateway.
        """

        self._log('Stopping...')

        # send the LoRa radio to sleep
        self.lora.callback(trigger=None, handler=None)
        self.lora.power_mode(LoRa.SLEEP)

        # stop the NTP sync
        self.rtc.ntp_sync(None)

        # cancel all the alarms
        self.stat_alarm.cancel()
        self.pull_alarm.cancel()

        # signal the UDP thread to stop
        self.udp_stop = True
        while self.udp_stop:
            utime.sleep_ms(50)

        # disable WLAN
        self.wlan.disconnect()
        self.wlan.deinit()

    def _connect_to_wifi(self):
        self.wlan.connect(self.ssid, auth=(None, self.password))
        while not self.wlan.isconnected():
            utime.sleep_ms(50)
        self._log('WiFi connected to: {}', self.ssid)

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _dr_to_bw(self, dr):
        bw = dr[-5:]
        if bw == 'BW125':
            return LoRa.BW_125KHZ
        elif bw == 'BW250':
            return LoRa.BW_250KHZ
        else:
            return LoRa.BW_500KHZ

    def _sf_bw_to_dr(self, sf, bw):
        dr = 'SF' + str(sf)
        if bw == LoRa.BW_125KHZ:
            return dr + 'BW125'
        elif bw == LoRa.BW_250KHZ:
            return dr + 'BW250'
        else:
            return dr + 'BW500'

    def _lora_cb(self, lora):
        """
        LoRa radio events callback handler.
        """

        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            if DEBUG:
                self._log("stats "+ujson.dumps(stats))
                self._log('rx_timestamp diff: {}', utime.ticks_diff(stats.rx_timestamp,utime.ticks_cpu()))
            packet = self._make_node_packet(rx_data, self.rtc.now(), stats.rx_timestamp, stats.sfrx, self.bw, stats.rssi, stats.snr)
            packet = self.frequency_rounding_fix(packet, self.frequency)
            self._log('Received and uploading packet: {}', packet)
            self._push_data(packet)
            self._log('after _push_data')
            self.rxfw += 1
            
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(
                mode=LoRa.LORA,
                region=self.region,
                frequency=self.frequency,
                bandwidth=self.bw,
                sf=self.sf,
                preamble=8,
                coding_rate=LoRa.CODING_4_5,
                power_mode=LoRa.ALWAYS_ON,
                #tx_iq=True
                )

    def _freq_to_float(self, frequency):
        """
        MicroPython has some inprecision when doing large float division.
        To counter this, this method first does integer division until we
        reach the decimal breaking point. This doesn't completely elimate
        the issue in all cases, but it does help for a number of commonly
        used frequencies.
        """

        divider = 6
        while divider > 0 and frequency % 10 == 0:
            frequency = frequency // 10
            divider -= 1
        if divider > 0:
            frequency = frequency / (10 ** divider)
        return frequency

    def frequency_rounding_fix(self, packet, frequency):
        freq = str(frequency)[0:3] + '.' + str(frequency)[3]

        start = packet.find("freq\":")
        end = packet.find(",", start)

        packet = packet[:start + 7] + freq + packet[end:]

        return packet

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return ujson.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, bw, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4], rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["freq"] = self._freq_to_float(self.frequency)
        RX_PK["rxpk"][0]["datr"] = self._sf_bw_to_dr(sf, bw)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = snr
        RX_PK["rxpk"][0]["data"] = ubinascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return ujson.dumps(RX_PK)

    def _push_data(self, data):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PUSH_DATA]) + ubinascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to push uplink packet to server: {}', ex)

    def _pull_data(self):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_DATA]) + ubinascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to pull downlink packets from server: {}', ex)

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = ujson.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_ACK]) + ubinascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('PULL RSP ACK exception: {}', ex)

    def _send_down_link(self, data, tmst, datarate, frequency):
        """
        Transmits a downlink message over LoRa.
        """
        self.lora.init(
            mode=LoRa.LORA,
            region=self.region,
            frequency=frequency,
            bandwidth=self._dr_to_bw(datarate),     # LoRa.BW_125KHZ
            sf=self._dr_to_sf(datarate),
            preamble=8,
            coding_rate=LoRa.CODING_4_5,
            power_mode=LoRa.ALWAYS_ON,
            #tx_iq=True
        )

        if WINDOW_COMPENSATION=='cycle':
            self.window_compensation = -((self.downlink_count % 25) * 1000)
        else:
            self.window_compensation = WINDOW_COMPENSATION

        t_adj = utime.ticks_add(tmst, self.window_compensation)
        self.lora_sock.settimeout(1)
        t_cpu = utime.ticks_cpu()
        self._log("BEFORE spin wait at {} late {}",t_cpu,t_cpu-tmst)
        while utime.ticks_diff(t_adj, utime.ticks_cpu()) > 0:
            pass
        t_cpu = utime.ticks_cpu()
        self._log("BEFORE lora_sock.send at {} late {} window_compensation {}",t_cpu,t_cpu-tmst,self.window_compensation)
        self.lora_sock.send(data)
        self._log("AFTER lora_sock.send late {}",utime.ticks_cpu()-tmst)
        self.lora_sock.setblocking(False)
        self._log(
            'Sent downlink packet scheduled on {}, at {:,d} Hz using {}: {}',
            tmst,
            frequency,
            datarate,
            data
        )
        self.downlink_count += 1

    def _send_down_link_class_c(self, data, datarate, frequency):
        self.lora.init(
            mode=LoRa.LORA,
            frequency=frequency,
            bandwidth=self._dr_to_bw(datarate),
            sf=self._dr_to_sf(datarate),
            preamble=8,
            coding_rate=LoRa.CODING_4_5,
            region=self.region,
            power_mode=LoRa.ALWAYS_ON,
            #tx_iq=True,
            device_class=LoRa.CLASS_C
            )

        self.lora_sock.send(data)
        self._log(
            'Sent downlink packet scheduled on {}, at {:.3f} Mhz using {}: {}',
            utime.ticks_cpu(),
            self._freq_to_float(frequency),
            datarate,
            data
        )

    def _udp_thread(self):
        """
        UDP thread, reads data from the server and handles it.
        """
        loops = 0
        while not self.udp_stop:
            if loops % 20 == 19:
                b4 = utime.ticks_cpu()
                gc.collect()
                self._log("gc.collect for {} us",utime.ticks_diff(utime.ticks_cpu(),b4))
            b4 = utime.ticks_cpu()
            utime.sleep_ms(UDP_THREAD_CYCLE_MS)
            t_diff = utime.ticks_diff(utime.ticks_cpu(),b4)
            if t_diff > (UDP_THREAD_CYCLE_MS*1000*1.5):
                self._log("overslept! for {} us",t_diff)
            try:
                b4 = utime.ticks_cpu()
                data, src = self.sock.recvfrom(1024)
                self._log("sock.recvfrom for {} us",utime.ticks_diff(utime.ticks_cpu(),b4))
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    self._log("Push ack")
                elif _type == PULL_ACK:
                    self._log("Pull ack")
                elif _type == PULL_RESP:
                    self._log("Pull resp")
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = ujson.loads(data[4:])
                    if DEBUG:
                       self._log("tx data "+ujson.dumps(tx_pk))
                    payload = ubinascii.a2b_base64(tx_pk["txpk"]["data"])
                    # depending on the board, pull the downlink message 1 or 6 ms upfronnt
                    
                    # tmst = utime.ticks_add(tx_pk["txpk"]["tmst"], self.window_compensation)
                    # t_us = utime.ticks_diff(utime.ticks_cpu(), utime.ticks_add(tmst, -15000))
                    tmst = tx_pk["txpk"]["tmst"]
                    t_req = utime.ticks_add(tmst, -RX_DELAY_TIMER_EARLY)
                    t_cpu = utime.ticks_cpu()
                    self._log("t_cpu {}",t_cpu)
                    t_us = utime.ticks_diff(t_req, t_cpu)
                    self._log("t_us {}",t_us)
                    if 1000 < t_us < 10000000:
                        self._log("Delaying for {} at {}, so should fire at t_req {}, compensated early_by {}",t_us,t_cpu,t_req,RX_DELAY_TIMER_EARLY)
                        def handler(x):
                            t_cpu = utime.ticks_cpu()
                            self._log("_send_down_link alarm fired at {} late {}us",t_cpu,t_cpu-t_req)
                            self._send_down_link(
                                payload,
                                tmst, tx_pk["txpk"]["datr"],
                                int(tx_pk["txpk"]["freq"] * 1000 + 0.0005) * 1000
                            )
                        self.uplink_alarm = Timer.Alarm(handler=handler, us=t_us)
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        self._log('Downlink timestamp error!, t_us: {}', t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    self._log("Pull rsp")
            except usocket.timeout:
                pass
            except OSError as ex:
                if ex.args[0] != errno.EAGAIN:
                    self._log('UDP recv OSError Exception: {}', ex)
            except Exception as ex:
                self._log('UDP recv Exception: {}', ex)

        # we are to close the socket
        self.sock.close()
        self.udp_stop = False
        self._log('UDP thread stopped')

    def _log(self, message, *args):
        """
        Outputs a log message to stdout.
        """
        if len(args)==0:
            print('[{}] '.format(utime.ticks_cpu()) + str(message))
        else:
            print('[{}] {}'.format(
                utime.ticks_cpu(),
                str(message).format(*args)
                ))
Example #12
0
    if (events & LoRa.RX_PACKET_EVENT):
        messageReceived = True
        socketLora.setblocking(True)
        Log.i("LoRa.RX_PACKET_EVENT")
        Led.blink_blue()
        data = socketLora.recv(256)
        socketLora.setblocking(True)
        _callback(data)
    #    if(events & LoRa.TX_PACKET_EVENT):
    #        Log.i("LoRa.TX_PACKET_EVENT")
    if (events & LoRa.TX_FAILED_EVENT):
        Log.i("LoRa.TX_FAILED_EVENT")
        _join()


lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_FAILED_EVENT),
              handler=_lora_callback)


def _join():
    global lopy_connected
    if not lora.has_joined():
        Log.i("Connecting Lora...")
        lopy_connected = False
        Led.blink_yellow()
        lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0)
        while not lora.has_joined():
            time.sleep(2.5)
        lopy_connected = True
        Led.blink_green()
        Log.i("Connected")
Example #13
0
class Lora_Link_Layer:

    def __init__(self, receive_msg_cb):
        print("Initializing Link Layer...")

        self.receive_msg_cb = receive_msg_cb
        self.msg_buffer_list = []

        self.incoming_cts_key = -1
        self.wait = False #Use other name, e.g. wait_for_channel
        self.wait_time = 0
        #use int instead of boolean. If multiple cts/rts arrive sum.

        self.lora = LoRa(mode=LoRa.LORA, region=LoRa.EU868)
        self.s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
        self.s.setblocking(False)
        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT), handler=self.lora_cb)

        self.lora_mac_address_b = binascii.hexlify(self.lora.mac())
        self.lora_mac_address = str(self.lora_mac_address_b, "utf-8")

        #_thread.start_new_thread(self.process_msg_pipeline, ())


    def get_lora_mac(self):
        return self.lora_mac_address


    def lora_cb(self, lora):
        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:

            msg = self.s.recv(250)
            #print(msg)
            #unpack message and pass meta information (like mac address or protocol) as parameter
            #meta, data = self.unpack_frame(msg.decode("utf-8"))
            #msg_utf8 = data
            #msg_1 = msg.decode("utf-8")
            #print('Link Layer: Lora packet received. ' + str(msg_utf8, "utf-8"))

            #if (msg_1.startswith("cts")):
            #    self.handle_incoming_cts(msg_1)

            #elif (msg_1.startswith("rts")):
            #    self.handle_incoming_rts(msg_1)
                #send cts. what if cts gets lost? Ignore? CSMA sending messages
                #only one rts at a time. identify rts if it is repeated.
            #else:
            print('Link Layer: Passing received data to Network Layer')
            self.receive_msg_cb(msg)

        #if events & LoRa.TX_PACKET_EVENT:
            #print('Lora packet sent')


    def append_msg_to_pipeline(self, msg, use_ca):
        #self.msg_buffer_list.append([msg, use_ca])
        self.lora_send_csma_ca(msg, use_ca)

        #Buffer Länge definieren und Nachrichten verwerfen oder auf Teilpackete aufteilen.
        #Time on air -> vielfaches (zufällig) warten, falls Kanal nicht frei.

    def process_msg_pipeline(self):
        while True:
            if (len(self.msg_buffer_list) > 0):
                msg_and_ca = self.msg_buffer_list.pop(0)
                msg = msg_and_ca[0]
                use_ca = msg_and_ca[1]

                self.lora_send_csma_ca(msg, use_ca)

                time.sleep(1)


    def lora_send_csma_ca(self, msg, use_ca):
        if (use_ca):
            print("Link Layer: using CA")

            #do not send rts if wait = true
            rts_random_key_b = binascii.hexlify(os.urandom(2))
            rts_random_key = str(rts_random_key_b, "utf-8")
            rts = "rts." + rts_random_key

            while not (rts_random_key == self.incoming_cts_key): #and not wait. Probably?
                #maximum repetition
                if not self.wait:
                    self.lora_send_csma(rts)
                    print("Link Layer: Waiting for cts. expected: " + str(rts_random_key) + " received: " + str(self.incoming_cts_key))
                else:
                    print("Link Layer: Waiting..." + str(self.wait_time))
                #change delay randomly/exponential
                #Wie lange warten? cts soll nicht mit nächstem rts versuch überlagert werden?
                time.sleep(2)

        else:
            print("Link Layer: NOT using CA")

        #blocking function
        self.lora_send_csma(msg)


    def lora_send_csma(self, msg):
        #Semtech LoRa: High sensitivity  -111 to -148dBm (Datasheet: https://semtech.my.salesforce.com/sfc/p/#E0000000JelG/a/2R0000001OKs/Bs97dmPXeatnbdoJNVMIDaKDlQz8q1N_gxDcgqi7g2o)
        while not self.lora.ischannel_free(-100,100):
            #max rep.
            print("Link Layer: channel not free")

        print("Link Layer: channel free (CSMA). Sending data...")
        self.lora_send(msg)


    def lora_send(self, msg):
        #frame = self.pack_frame("c", msg)
        frame = msg
        print("Link Layer | Sending data: " + str(frame))
        self.s.send(frame)


    def handle_incoming_cts(self, incoming_cts):
        print("Link Layer: CTS received. Key=" + incoming_cts)
        self.incoming_cts_key = str(incoming_cts.split(".")[1], "utf-8")
        #Important: Wait also if CTS is for other lora. Use MAC adress as identifier
        #Combine with incoming RTS


    def handle_incoming_rts(self, incoming_rts):
        incoming_rts_key = str(incoming_rts.split(".")[1], "utf-8")
        print("Link Layer: RTS received. Key=" + incoming_rts_key)
        #send cts. what if cts gets lost? Ignore? CSMA sending messages
        #only one rts at a time. identify rts if it is repeated.
        #check if rts is new. Problem: other lora did not receive cts. Important: Waiting time
        if (not self.wait):
            _thread.start_new_thread(self.wait_timer, (5,))
            print("Link Layer: CTS other lora. Waiting for other lora...")
            #save mac address of other lora and wait until packet from this lora arrived or max
            cts = "cts." + incoming_rts_key
            #self.lora_send_csma(cts.encode("utf-8"))
            self.lora_send_csma(cts)


    def wait_timer(self, wait_time):
        self.wait_time = wait_time
        self.wait = True
        print("Wait timer")

        while self.wait_time > 0:
            time.sleep(1)
            self.wait_time = self.wait_time - 1
            print(str(self.wait_time))

        self.wait = False


    #This field states whether the frame is a data frame or it is used for control functions like error and flow control or link management etc.
    def pack_frame(self, type, data):
        #Use single bits for control and 8 bytes for MAC without delimiters. character delimiters can cause problems and need much space.
        frame = type + "::" + self.get_lora_mac() + "::" + data
        print("Link Layer:" + frame)
        return frame


    def unpack_frame(self, frame):
        meta = [frame.split("::")[0], frame.split("::")[1]]
        data = frame.split("::")[2]
        return meta, data
class NanoGateway:
    """
    Nano gateway class, set up by default for use with TTN, but can be configured
    for any other network supporting the Semtech Packet Forwarder.
    Only required configuration is wifi_ssid and wifi_password which are used for
    connecting to the Internet.
    """
    def __init__(self,
                 id,
                 frequency,
                 datarate,
                 ssid,
                 password,
                 server,
                 port,
                 ntp_server='pool.ntp.org',
                 ntp_period=3600):
        self.id = id
        self.server = server
        self.port = port

        self.frequency = frequency
        self.datarate = datarate

        self.ssid = ssid
        self.password = password

        self.ntp_server = ntp_server
        self.ntp_period = ntp_period

        self.server_ip = None

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.sf = self._dr_to_sf(self.datarate)
        self.bw = self._dr_to_bw(self.datarate)

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.wlan = None
        self.sock = None
        self.udp_stop = False
        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

        self.rtc = machine.RTC()

    def start(self):
        """
        Starts the LoRaWAN nano gateway.
        """

        self._log('Starting LoRaWAN nano gateway with id: {}', self.id)

        # setup WiFi as a station and connect
        self.wlan = WLAN(mode=WLAN.STA)
        self._connect_to_wifi()

        # get a time sync
        self._log('Syncing time with {} ...', self.ntp_server)
        self.rtc.ntp_sync(self.ntp_server, update_period=self.ntp_period)
        while not self.rtc.synced():
            utime.sleep_ms(50)
        self._log("RTC NTP sync complete")

        # get the server IP and create an UDP socket
        self.server_ip = usocket.getaddrinfo(self.server, self.port)[0][-1]
        self._log('Opening UDP socket to {} ({}) port {}...', self.server,
                  self.server_ip[0], self.server_ip[1])
        self.sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM,
                                   usocket.IPPROTO_UDP)
        self.sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # push the first time immediatelly
        self._push_data(self._make_stat_packet())

        # create the alarms
        self.stat_alarm = Timer.Alarm(
            handler=lambda t: self._push_data(self._make_stat_packet()),
            s=60,
            periodic=True)
        self.pull_alarm = Timer.Alarm(handler=lambda u: self._pull_data(),
                                      s=25,
                                      periodic=True)

        # start the UDP receive thread
        self.udp_stop = False
        _thread.start_new_thread(self._udp_thread, ())

        # initialize the LoRa radio in LORA mode
        self._log('Setting up the LoRa radio at {} Mhz using {}',
                  self._freq_to_float(self.frequency), self.datarate)
        self.lora = LoRa(mode=LoRa.LORA,
                         frequency=self.frequency,
                         bandwidth=self.bw,
                         sf=self.sf,
                         preamble=8,
                         coding_rate=LoRa.CODING_4_5,
                         tx_iq=True)

        # create a raw LoRa socket
        self.lora_sock = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW)
        self.lora_sock.setblocking(False)
        self.lora_tx_done = False

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT
                                    | LoRa.TX_PACKET_EVENT),
                           handler=self._lora_cb)
        self._log('LoRaWAN nano gateway online')

    def stop(self):
        """
        Stops the LoRaWAN nano gateway.
        """

        self._log('Stopping...')

        # send the LoRa radio to sleep
        self.lora.callback(trigger=None, handler=None)
        self.lora.power_mode(LoRa.SLEEP)

        # stop the NTP sync
        self.rtc.ntp_sync(None)

        # cancel all the alarms
        self.stat_alarm.cancel()
        self.pull_alarm.cancel()

        # signal the UDP thread to stop
        self.udp_stop = True
        while self.udp_stop:
            utime.sleep_ms(50)

        # disable WLAN
        self.wlan.disconnect()
        self.wlan.deinit()

    def _connect_to_wifi(self):
        self.wlan.connect(self.ssid, auth=(None, self.password))
        while not self.wlan.isconnected():
            utime.sleep_ms(50)
        self._log('WiFi connected to: {}', self.ssid)

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _dr_to_bw(self, dr):
        bw = dr[-5:]
        if bw == 'BW125':
            return LoRa.BW_125KHZ
        elif bw == 'BW250':
            return LoRa.BW_250KHZ
        else:
            return LoRa.BW_500KHZ

    def _sf_bw_to_dr(self, sf, bw):
        dr = 'SF' + str(sf)
        if bw == LoRa.BW_125KHZ:
            return dr + 'BW125'
        elif bw == LoRa.BW_250KHZ:
            return dr + 'BW250'
        else:
            return dr + 'BW500'

    def _lora_cb(self, lora):
        """
        LoRa radio events callback handler.
        """

        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            packet = self._make_node_packet(rx_data, self.rtc.now(),
                                            stats.rx_timestamp, stats.sfrx,
                                            self.bw, stats.rssi, stats.snr)
            self._push_data(packet)
            self._log('Received packet: {}', packet)
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(mode=LoRa.LORA,
                      frequency=self.frequency,
                      bandwidth=self.bw,
                      sf=self.sf,
                      preamble=8,
                      coding_rate=LoRa.CODING_4_5,
                      tx_iq=True)

    def _freq_to_float(self, frequency):
        """
        MicroPython has some inprecision when doing large float division.
        To counter this, this method first does integer division until we
        reach the decimal breaking point. This doesn't completely elimate
        the issue in all cases, but it does help for a number of commonly
        used frequencies.
        """

        divider = 6
        while divider > 0 and frequency % 10 == 0:
            frequency = frequency // 10
            divider -= 1
        if divider > 0:
            frequency = frequency / (10**divider)
        return frequency

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (
            now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return ujson.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, bw, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (
            rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4],
            rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["freq"] = self._freq_to_float(self.frequency)
        RX_PK["rxpk"][0]["datr"] = self._sf_bw_to_dr(sf, bw)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = snr
        RX_PK["rxpk"][0]["data"] = ubinascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return ujson.dumps(RX_PK)

    def _push_data(self, data):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PUSH_DATA]) + ubinascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to push uplink packet to server: {}', ex)

    def _pull_data(self):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_DATA]) + ubinascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to pull downlink packets from server: {}',
                          ex)

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = ujson.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_ACK]) + ubinascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('PULL RSP ACK exception: {}', ex)

    def _send_down_link(self, data, tmst, datarate, frequency):
        """
        Transmits a downlink message over LoRa.
        """

        self.lora.init(mode=LoRa.LORA,
                       frequency=frequency,
                       bandwidth=self._dr_to_bw(datarate),
                       sf=self._dr_to_sf(datarate),
                       preamble=8,
                       coding_rate=LoRa.CODING_4_5,
                       tx_iq=True)
        while utime.ticks_cpu() < tmst:
            pass
        self.lora_sock.send(data)
        self._log(
            'Sent downlink packet scheduled on {:.3f}, at {:.3f} Mhz using {}: {}',
            tmst / 1000000, self._freq_to_float(frequency), datarate, data)

    def _udp_thread(self):
        """
        UDP thread, reads data from the server and handles it.
        """

        while not self.udp_stop:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    self._log("Push ack")
                elif _type == PULL_ACK:
                    self._log("Pull ack")
                elif _type == PULL_RESP:
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = ujson.loads(data[4:])
                    tmst = tx_pk["txpk"]["tmst"]
                    t_us = tmst - utime.ticks_cpu() - 15000
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = Timer.Alarm(
                            handler=lambda x: self._send_down_link(
                                ubinascii.a2b_base64(tx_pk["txpk"]["data"]),
                                tx_pk["txpk"]["tmst"] - 50, tx_pk["txpk"][
                                    "datr"],
                                int(tx_pk["txpk"]["freq"] * 1000) * 1000),
                            us=t_us)
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        self._log('Downlink timestamp error!, t_us: {}', t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    self._log("Pull rsp")
            except usocket.timeout:
                pass
            except OSError as ex:
                if ex.errno != errno.EAGAIN:
                    self._log('UDP recv OSError Exception: {}', ex)
            except Exception as ex:
                self._log('UDP recv Exception: {}', ex)

            # wait before trying to receive again
            utime.sleep_ms(UDP_THREAD_CYCLE_MS)

        # we are to close the socket
        self.sock.close()
        self.udp_stop = False
        self._log('UDP thread stopped')

    def _log(self, message, *args):
        """
        Outputs a log message to stdout.
        """

        print('[{:>10.3f}] {}'.format(utime.ticks_ms() / 1000,
                                      str(message).format(*args)))
class NanoGateway:
    """
    Nano gateway class, set up by default for use with TTN, but can be configured
    for any other network supporting the Semtech Packet Forwarder.
    Only required configuration is wifi_ssid and wifi_password which are used for
    connecting to the Internet.
    """
    def __init__(self,
                 id,
                 frequency,
                 datarate,
                 server,
                 port,
                 ntp_server='pool.ntp.org',
                 ntp_period=3600):
        self.id = id
        self.server = server
        self.port = port

        self.frequency = frequency
        self.datarate = datarate

        # self.ssid = ssid
        # self.password = password

        self.ntp_server = ntp_server
        self.ntp_period = ntp_period

        self.server_ip = None

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.sf = self._dr_to_sf(self.datarate)
        self.bw = self._dr_to_bw(self.datarate)

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.lte = None
        self.sock = None
        self.udp_stop = False
        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

        self.rtc = machine.RTC()

    def start(self):
        """
        Starts the LoRaWAN nano gateway.
        """

        pycom.heartbeat(False)

        self._log('Starting LoRaWAN nano gateway with id: {}', self.id)

        # # setup WiFi as a station and connect
        # self.wlan = WLAN(mode=WLAN.STA)
        # self._connect_to_wifi()

        # setup LTE CATM1 connection
        self.lte = LTE(carrier="verizon")
        self._connect_to_LTE()

        # get a time sync
        self._log('Syncing time with {} ...', self.ntp_server)
        self.rtc.ntp_sync(self.ntp_server, update_period=self.ntp_period)
        while not self.rtc.synced():
            utime.sleep_ms(50)
        self._log("RTC NTP sync complete")

        # get the server IP and create an UDP socket
        self.server_ip = usocket.getaddrinfo(self.server, self.port)[0][-1]
        self._log('Opening UDP socket to {} ({}) port {}...', self.server,
                  self.server_ip[0], self.server_ip[1])
        self.sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM,
                                   usocket.IPPROTO_UDP)
        self.sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # push the first time immediatelly
        self._push_data(self._make_stat_packet())

        # create the alarms
        self.stat_alarm = Timer.Alarm(
            handler=lambda t: self._push_data(self._make_stat_packet()),
            s=60,
            periodic=True)
        self.pull_alarm = Timer.Alarm(handler=lambda u: self._pull_data(),
                                      s=25,
                                      periodic=True)

        # start the UDP receive thread
        self.udp_stop = False
        _thread.start_new_thread(self._udp_thread, ())

        # initialize the LoRa radio in LORA mode
        self._log('Setting up the LoRa radio at {} Mhz using {}',
                  self._freq_to_float(self.frequency), self.datarate)
        self.lora = LoRa(mode=LoRa.LORA,
                         frequency=self.frequency,
                         bandwidth=self.bw,
                         sf=self.sf,
                         preamble=8,
                         coding_rate=LoRa.CODING_4_5,
                         tx_iq=True)

        # create a raw LoRa socket
        self.lora_sock = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW)
        self.lora_sock.setblocking(False)
        self.lora_tx_done = False

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT
                                    | LoRa.TX_PACKET_EVENT),
                           handler=self._lora_cb)
        self._log('LoRaWAN nano gateway online')

    def stop(self):
        """
        Stops the LoRaWAN nano gateway.
        """

        self._log('Stopping...')

        # send the LoRa radio to sleep
        self.lora.callback(trigger=None, handler=None)
        self.lora.power_mode(LoRa.SLEEP)

        # stop the NTP sync
        self.rtc.ntp_sync(None)

        # cancel all the alarms
        self.stat_alarm.cancel()
        self.pull_alarm.cancel()

        # signal the UDP thread to stop
        self.udp_stop = True
        while self.udp_stop:
            utime.sleep_ms(50)

        # disable LTE
        self.lte.disconnect()
        self.lte.dettach()

    def _connect_to_wifi(self):
        self.wlan.connect(self.ssid, auth=(None, self.password))
        while not self.wlan.isconnected():
            utime.sleep_ms(50)
        self._log('WiFi connected to: {}', self.ssid)

    def _connect_to_LTE(self):
        print("reset modem")
        try:
            self.lte.reset()
        except:
            print("Exception during reset")

        print("delay 5 secs")
        utime.sleep(5.0)

        if self.lte.isattached():
            try:
                print("LTE was already attached, disconnecting...")
                if self.lte.isconnected():
                    print("disconnect")
                    self.lte.disconnect()
            except:
                print("Exception during disconnect")

            try:
                if self.lte.isattached():
                    print("detach")
                    self.lte.dettach()
            except:
                print("Exception during dettach")

            try:
                print("resetting modem...")
                self.lte.reset()
            except:
                print("Exception during reset")

            print("delay 5 secs")
            utime.sleep(5.0)

        # enable network registration and location information, unsolicited result code
        self.at('AT+CEREG=2')

        # print("full functionality level")
        self.at('AT+CFUN=1')
        utime.sleep(1.0)

        # using Hologram SIM
        self.at('AT+CGDCONT=1,"IP","hologram"')

        print("attempt to attach cell modem to base station...")
        # lte.attach()  # do not use attach with custom init for Hologram SIM

        self.at("ATI")
        utime.sleep(2.0)

        i = 0
        while self.lte.isattached() == False:
            # get EPS Network Registration Status:
            # +CEREG: <stat>[,[<tac>],[<ci>],[<AcT>]]
            # <tac> values:
            # 0 - not registered
            # 1 - registered, home network
            # 2 - not registered, but searching...
            # 3 - registration denied
            # 4 - unknown (out of E-UTRAN coverage)
            # 5 - registered, roaming
            r = self.at('AT+CEREG?')
            try:
                r0 = r[0]  # +CREG: 2,<tac>
                r0x = r0.split(',')  # ['+CREG: 2',<tac>]
                tac = int(r0x[1])  # 0..5
                print("tac={}".format(tac))
            except IndexError:
                tac = 0
                print("Index Error!!!")

            # get signal strength
            # +CSQ: <rssi>,<ber>
            # <rssi>: 0..31, 99-unknown
            r = self.at('AT+CSQ')

            # extended error report
            # r = at('AT+CEER')

            if self.lte.isattached():
                print("Modem attached (isattached() function worked)!!!")
                break

            if (tac == 1) or (tac == 5):
                print("Modem attached!!!")
                break

            i = i + 5
            print("not attached: {} secs".format(i))

            if (tac != 0):
                self.blink(BLUE, tac)
            else:
                self.blink(RED, 5)

            utime.sleep(2)

        self.at('AT+CEREG?')
        print("connect: start a data session and obtain an IP address")
        self.lte.connect(cid=3)
        i = 0
        while not self.lte.isconnected():
            i = i + 1
            print("not connected: {}".format(i))
            self.blink(YELLOW, 1)
            utime.sleep(1.0)

        print("connected!!!")
        pycom.rgbled(GREEN)

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _dr_to_bw(self, dr):
        bw = dr[-5:]
        if bw == 'BW125':
            return LoRa.BW_125KHZ
        elif bw == 'BW250':
            return LoRa.BW_250KHZ
        else:
            return LoRa.BW_500KHZ

    def _sf_bw_to_dr(self, sf, bw):
        dr = 'SF' + str(sf)
        if bw == LoRa.BW_125KHZ:
            return dr + 'BW125'
        elif bw == LoRa.BW_250KHZ:
            return dr + 'BW250'
        else:
            return dr + 'BW500'

    def _lora_cb(self, lora):
        """
        LoRa radio events callback handler.
        """

        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            packet = self._make_node_packet(rx_data, self.rtc.now(),
                                            stats.rx_timestamp, stats.sfrx,
                                            self.bw, stats.rssi, stats.snr)
            self._push_data(packet)
            self._log('Received packet: {}', packet)
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(mode=LoRa.LORA,
                      frequency=self.frequency,
                      bandwidth=self.bw,
                      sf=self.sf,
                      preamble=8,
                      coding_rate=LoRa.CODING_4_5,
                      tx_iq=True)

    def _freq_to_float(self, frequency):
        """
        MicroPython has some inprecision when doing large float division.
        To counter this, this method first does integer division until we
        reach the decimal breaking point. This doesn't completely elimate
        the issue in all cases, but it does help for a number of commonly
        used frequencies.
        """

        divider = 6
        while divider > 0 and frequency % 10 == 0:
            frequency = frequency // 10
            divider -= 1
        if divider > 0:
            frequency = frequency / (10**divider)
        return frequency

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (
            now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return ujson.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, bw, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (
            rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4],
            rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["freq"] = self._freq_to_float(self.frequency)
        RX_PK["rxpk"][0]["datr"] = self._sf_bw_to_dr(sf, bw)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = snr
        RX_PK["rxpk"][0]["data"] = ubinascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return ujson.dumps(RX_PK)

    def _push_data(self, data):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PUSH_DATA]) + ubinascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to push uplink packet to server: {}', ex)

    def _pull_data(self):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_DATA]) + ubinascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to pull downlink packets from server: {}',
                          ex)

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = ujson.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_ACK]) + ubinascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('PULL RSP ACK exception: {}', ex)

    def _send_down_link(self, data, tmst, datarate, frequency):
        """
        Transmits a downlink message over LoRa.
        """

        self.lora.init(mode=LoRa.LORA,
                       frequency=frequency,
                       bandwidth=self._dr_to_bw(datarate),
                       sf=self._dr_to_sf(datarate),
                       preamble=8,
                       coding_rate=LoRa.CODING_4_5,
                       tx_iq=True)
        while utime.ticks_cpu() < tmst:
            pass
        self.lora_sock.send(data)
        self._log(
            'Sent downlink packet scheduled on {:.3f}, at {:.3f} Mhz using {}: {}',
            tmst / 1000000, self._freq_to_float(frequency), datarate, data)

    def _udp_thread(self):
        """
        UDP thread, reads data from the server and handles it.
        """

        while not self.udp_stop:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    self._log("Push ack")
                elif _type == PULL_ACK:
                    self._log("Pull ack")
                elif _type == PULL_RESP:
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = ujson.loads(data[4:])
                    tmst = tx_pk["txpk"]["tmst"]
                    t_us = tmst - utime.ticks_cpu() - 15000
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = Timer.Alarm(
                            handler=lambda x: self._send_down_link(
                                ubinascii.a2b_base64(tx_pk["txpk"]["data"]),
                                tx_pk["txpk"]["tmst"] - 50, tx_pk["txpk"][
                                    "datr"],
                                int(tx_pk["txpk"]["freq"] * 1000) * 1000),
                            us=t_us)
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        self._log('Downlink timestamp error!, t_us: {}', t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    self._log("Pull rsp")
            except usocket.timeout:
                pass
            except OSError as ex:
                if ex.errno != errno.EAGAIN:
                    self._log('UDP recv OSError Exception: {}', ex)
            except Exception as ex:
                self._log('UDP recv Exception: {}', ex)

            # wait before trying to receive again
            utime.sleep_ms(UDP_THREAD_CYCLE_MS)

        # we are to close the socket
        self.sock.close()
        self.udp_stop = False
        self._log('UDP thread stopped')

    def _log(self, message, *args):
        """
        Outputs a log message to stdout.
        """

        print('[{:>10.3f}] {}'.format(utime.ticks_ms() / 1000,
                                      str(message).format(*args)))

    def at(self, cmd):
        print("modem command: {}".format(cmd))
        r = self.lte.send_at_cmd(cmd).split('\r\n')
        r = list(filter(None, r))
        print("response={}".format(r))
        return r

    def blink(self, rgb, n):
        for i in range(n):
            pycom.rgbled(rgb)
            utime.sleep(0.25)
            pycom.rgbled(BLACK)
            utime.sleep(0.1)
Example #16
0
class NanoGateway:
    """
    Nano gateway class, set up by default for use with TTN, but can be configured
    for any other network supporting the Semtech Packet Forwarder.

    Only required configuration is wifi_ssid and wifi_password which are used for
    connecting to the Internet.
    """

    PROTOCOL_VERSION = const(2)

    PUSH_DATA = const(0)
    PUSH_ACK = const(1)
    PULL_DATA = const(2)
    PULL_ACK = const(4)
    PULL_RESP = const(3)

    TX_ERR_NONE = 'NONE'
    TX_ERR_TOO_LATE = 'TOO_LATE'
    TX_ERR_TOO_EARLY = 'TOO_EARLY'
    TX_ERR_COLLISION_PACKET = 'COLLISION_PACKET'
    TX_ERR_COLLISION_BEACON = 'COLLISION_BEACON'
    TX_ERR_TX_FREQ = 'TX_FREQ'
    TX_ERR_TX_POWER = 'TX_POWER'
    TX_ERR_GPS_UNLOCKED = 'GPS_UNLOCKED'

    UDP_THREAD_CYCLE_MS = const(10)

    STAT_PK = {
        'stat': {
            'time': '',
            'lati': 0,
            'long': 0,
            'alti': 0,
            'rxnb': 0,
            'rxok': 0,
            'rxfw': 0,
            'ackr': 100.0,
            'dwnb': 0,
            'txnb': 0
        }
    }

    RX_PK = {
        'rxpk': [{
            'time': '',
            'tmst': 0,
            'chan': 0,
            'rfch': 0,
            'freq': 0,
            'stat': 1,
            'modu': 'LORA',
            'datr': '',
            'codr': '4/5',
            'rssi': 0,
            'lsnr': 0,
            'size': 0,
            'data': ''
        }]
    }

    TX_ACK_PK = {'txpk_ack': {'error': ''}}

    def __init__(self,
                 wifi_ssid,
                 wifi_password,
                 gateway_id=None,
                 server='router.eu.thethings.network',
                 port=1700,
                 frequency=868100000,
                 datarate='SF7BW125',
                 ntp_server='pool.ntp.org',
                 ntp_period=3600):
        # If unset, set the Gateway ID to be the first 3 bytes
        # of MAC address + 'FFFE' + last 3 bytes of MAC address
        if gateway_id is None:
            gateway_id = ubinascii.hexlify(machine.unique_id()).upper()
            gateway_id = gateway_id[:6] + 'FFFE' + gateway_id[6:12]
        self.gateway_id = gateway_id

        self.server = server
        self.port = port

        self.frequency = frequency
        self.datarate = datarate

        self.wifi_ssid = wifi_ssid
        self.wifi_password = wifi_password

        self.ntp_server = ntp_server
        self.ntp_period = ntp_period

        self.server_ip = None

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.sf = self._dr_to_sf(self.datarate)
        self.bw = self._dr_to_bw(self.datarate)

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.wlan = None
        self.sock = None
        self.udp_stop = False
        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

        self.rtc = machine.RTC()

    def start(self):
        """
        Starts the nano gateway.
        """

        self.log('Starting nano gateway with id {}', self.gateway_id)

        # Change WiFi to STA mode and connect
        self.wlan = WLAN(mode=WLAN.STA)
        self._connect_to_wifi()

        # Get a time sync
        self.log('Syncing time with {} ...', self.ntp_server)
        self.rtc.ntp_sync(self.ntp_server, update_period=self.ntp_period)
        while not self.rtc.synced():
            utime.sleep_ms(50)
        self.log('RTC NTP sync complete')

        # Get the server IP and create an UDP socket
        self.server_ip = usocket.getaddrinfo(self.server, self.port)[0][-1]
        self.log('Opening UDP socket to {} ({}) port {}...', self.server,
                 self.server_ip[0], self.server_ip[1])
        self.sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM,
                                   usocket.IPPROTO_UDP)
        self.sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # Push the first time stat immediately
        self._push_data(self._make_stat_packet())

        # Create the alarms
        self.stat_alarm = machine.Timer.Alarm(
            handler=lambda t: self._push_data(self._make_stat_packet()),
            s=60,
            periodic=True)
        self.pull_alarm = machine.Timer.Alarm(
            handler=lambda u: self._pull_data(), s=25, periodic=True)

        # Start the UDP receive thread
        self.udp_stop = False
        _thread.start_new_thread(self._udp_thread, ())

        # Initialize the LoRa radio in LORA mode
        self.log('Setting up LoRa socket on {:.1f} Mhz using {}',
                 self._freq_to_float(self.frequency), self.datarate)
        self.lora = LoRa(mode=LoRa.LORA,
                         frequency=self.frequency,
                         bandwidth=self.bw,
                         sf=self.sf,
                         preamble=8,
                         coding_rate=LoRa.CODING_4_5,
                         tx_iq=True)

        # Create a raw LoRa socket
        self.lora_sock = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW)
        self.lora_sock.setblocking(False)

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT
                                    | LoRa.TX_PACKET_EVENT),
                           handler=self._lora_cb)
        self.log('Nano gateway online')

    def stop(self):
        """
        Stops the nano gateway.
        """

        self.log('Stopping...')

        # Send the LoRa radio to sleep
        self.lora.callback(trigger=None, handler=None)
        self.lora.power_mode(LoRa.SLEEP)

        # Stop the NTP sync
        self.rtc.ntp_sync(None)

        # Cancel all the alarms
        self.stat_alarm.cancel()
        self.pull_alarm.cancel()

        # Signal the UDP thread to stop
        self.udp_stop = True
        while self.udp_stop:
            utime.sleep_ms(50)

        # Disable WLAN
        self.wlan.disconnect()
        self.wlan.deinit()

    def _connect_to_wifi(self):
        self.wlan.connect(self.wifi_ssid, auth=(None, self.wifi_password))
        while not self.wlan.isconnected():
            utime.sleep_ms(50)
        self.log('WiFi connected: {}', self.wifi_ssid)

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _dr_to_bw(self, dr):
        bw = dr[-5:]
        if bw == 'BW125':
            return LoRa.BW_125KHZ
        elif bw == 'BW250':
            return LoRa.BW_250KHZ
        else:
            return LoRa.BW_500KHZ

    def _sf_bw_to_dr(self, sf, bw):
        dr = 'SF' + str(sf)
        if bw == LoRa.BW_125KHZ:
            return dr + 'BW125'
        elif bw == LoRa.BW_250KHZ:
            return dr + 'BW250'
        else:
            return dr + 'BW500'

    def _lora_cb(self, lora):
        """
        Event listener for LoRa radio events.
        """

        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            packet = self._make_node_packet(rx_data, self.rtc.now(),
                                            stats.rx_timestamp, stats.sfrx,
                                            self.bw, stats.rssi, stats.snr)
            self.log('Received packet: {}', packet)
            self._push_data(packet)
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.log('Re-initing LoRa radio after transmission')
            self.txnb += 1
            lora.init(mode=LoRa.LORA,
                      frequency=self.frequency,
                      bandwidth=self.bw,
                      sf=self.sf,
                      preamble=8,
                      coding_rate=LoRa.CODING_4_5,
                      tx_iq=True)

    def _freq_to_float(self, frequency):
        """
        MicroPython has some inprecision when doing large float division.

        To counter this, this method first does integer division until we
        reach the decimal breaking point. This doesn't completely elimate
        the issue in all cases, but it does help for a number of commonly
        used frequencies.
        """

        divider = 6
        while divider > 0 and frequency % 10 == 0:
            frequency = frequency // 10
            divider -= 1
        if divider > 0:
            frequency = frequency / (10**divider)
        return frequency

    def _make_stat_packet(self):
        now = self.rtc.now()
        self.STAT_PK['stat']['time'] = '%d-%02d-%02d %02d:%02d:%02d GMT' % (
            now[0], now[1], now[2], now[3], now[4], now[5])
        self.STAT_PK['stat']['rxnb'] = self.rxnb
        self.STAT_PK['stat']['rxok'] = self.rxok
        self.STAT_PK['stat']['rxfw'] = self.rxfw
        self.STAT_PK['stat']['dwnb'] = self.dwnb
        self.STAT_PK['stat']['txnb'] = self.txnb
        return ujson.dumps(self.STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, bw, rssi, snr):
        self.RX_PK['rxpk'][0]['time'] = '%d-%02d-%02dT%02d:%02d:%02d.%dZ' % (
            rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4],
            rx_time[5], rx_time[6])
        self.RX_PK['rxpk'][0]['tmst'] = tmst
        self.RX_PK['rxpk'][0]['freq'] = self._freq_to_float(self.frequency)
        self.RX_PK['rxpk'][0]['datr'] = self._sf_bw_to_dr(sf, bw)
        self.RX_PK['rxpk'][0]['rssi'] = rssi
        self.RX_PK['rxpk'][0]['lsnr'] = float(snr)
        self.RX_PK['rxpk'][0]['data'] = ubinascii.b2a_base64(rx_data)[:-1]
        self.RX_PK['rxpk'][0]['size'] = len(rx_data)
        return ujson.dumps(self.RX_PK)

    def _push_data(self, data):
        token = uos.urandom(2)
        packet = bytes([self.PROTOCOL_VERSION]) + token + bytes(
            [self.PUSH_DATA]) + ubinascii.unhexlify(self.gateway_id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except BaseException as ex:
                self.log('Failed to push uplink packet to server: {}', ex)

    def _pull_data(self):
        token = uos.urandom(2)
        packet = bytes([self.PROTOCOL_VERSION]) + token + bytes(
            [self.PULL_DATA]) + ubinascii.unhexlify(self.gateway_id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except BaseException as ex:
                self.log('Failed to pull downlink packets from server: {}', ex)

    def _ack_pull_rsp(self, token, error):
        self.TX_ACK_PK['txpk_ack']['error'] = error
        resp = ujson.dumps(self.TX_ACK_PK)
        packet = bytes([self.PROTOCOL_VERSION]) + token + bytes(
            [self.PULL_ACK]) + ubinascii.unhexlify(self.gateway_id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except BaseException as ex:
                self.log('PULL RSP ACK exception: {}', ex)

    def _send_down_link(self, data, tmst, datarate, frequency):
        """
        Transmits a downlink message over LoRa.
        """

        self.lora.init(mode=LoRa.LORA,
                       frequency=frequency,
                       bandwidth=self._dr_to_bw(datarate),
                       sf=self._dr_to_sf(datarate),
                       preamble=8,
                       coding_rate=LoRa.CODING_4_5,
                       tx_iq=True)
        while utime.ticks_us() < tmst:
            pass
        self.lora_sock.send(data)
        self.log(
            'Sent downlink packet scheduled for {:.3f}, at {:.1f} Mhz using {}: {}',
            tmst / 1000000, self._freq_to_float(frequency), datarate, data)

    def _udp_thread(self):
        """
        UDP thread, reads data from the server and handles it.
        """

        while not self.udp_stop:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == self.PUSH_ACK:
                    self.log('Push ack')
                elif _type == self.PULL_ACK:
                    self.log('Pull ack')
                elif _type == self.PULL_RESP:
                    self.dwnb += 1
                    ack_error = self.TX_ERR_NONE
                    tx_pk = ujson.loads(data[4:])
                    tmst = tx_pk['txpk']['tmst']
                    t_us = tmst - utime.ticks_us() - 12500
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = machine.Timer.Alarm(
                            handler=lambda x: self._send_down_link(
                                ubinascii.a2b_base64(tx_pk['txpk']['data']),
                                tx_pk['txpk']['tmst'] - 50, tx_pk['txpk'][
                                    'datr'],
                                int(tx_pk['txpk']['freq'] * 1000000)),
                            us=t_us)
                    else:
                        ack_error = self.TX_ERR_TOO_LATE
                        self.log('Downlink timestamp error!, t_us: {}', t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    self.log('Pull rsp')
                else:
                    self.log('Unknown message type from server: {}', _type)
            except usocket.timeout:
                pass
            except OSError as ex:
                if ex.errno != errno.EAGAIN:
                    self.log('UDP recv OSError Exception: {}', ex)
            except BaseException as ex:
                self.log('UDP recv Exception: {}', ex)

            # Wait before trying to receive again
            utime.sleep_ms(self.UDP_THREAD_CYCLE_MS)

        self.sock.close()
        self.udp_stop = False
        self.log('UDP thread stopped')

    def log(self, message, *args):
        """
        Prints a log message to the stdout.
        """

        print('[{:>10.3f}] {}'.format(utime.ticks_ms() / 1000,
                                      str(message).format(*args)))
class LoraCoverage:
    def __init__(self, host, port, ssid, wpa, log_path):
        self.host = host
        self.port = port
        self.ssid = ssid
        self.wpa = wpa
        self.path = log_path

        self.gps_buffer = None
        self.log_time = None
        self.log_file = None
        self.rxnb = 0

        self.loramac = binascii.hexlify(network.LoRa().mac())

        # to be refactored
        self.end = False

        self.lock = _thread.allocate_lock()

        # Create proper directory for log file
        self._init_log()

        # Setup wypy/lopy as a station
        self.wlan = network.WLAN(mode=network.WLAN.STA)
        self.wlan.connect(self.ssid, auth=(network.WLAN.WPA2, self.wpa))

        while not self.wlan.isconnected():
            print('Connecting to Android WIFI HOTPOST...')
            time.sleep(1)

        print('Connected to Android WIFI HOTPOST')

        # Lora socket
        self.lora = None
        self.lora_sock = None

        # TCP socket
        self.tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Connect
        self.tcp_sock.connect((self.host, self.port))

    def _init_log(self):
        # create directory log if not exist
        try:
            os.mkdir(self.path)
        except OSError:
            pass

    # Check if string is empty
    def _isNotBlank(self, myString):
        return bool(myString and myString.strip())

    def _get_log_name(self):
        #print(self.log_time[:19])
        #print(type(ts))
        # ts = ts.replace('-', '')
        # ts = ts.replace('T', '')
        # ts = ts.replace(':', '')
        #ts = '1974'
        #base = self.path + '/acq/'
        #return base + ts + 'list.csv'
        return self.path + '/acq.list.csv'

    def _lora_cb(self, lora):
        events = lora.events()
        if (events & LoRa.RX_PACKET_EVENT) and (self.gps_buffer is not None):
            #if self.log_file is None:
            #    self.log_file = open(self._get_log_name(), 'w')

            data_rx = self.lora_sock.recv(256)
            stats = self.lora.stats()  # get lora stats (data is tuple)

            if self._isNotBlank(data_rx):
                with self.lock:
                    print(self.log_time)
                    print(self.gps_buffer)
                #print(self.gps_buffer['time'])
                # time_stamp = stats[0]          # get timestamp value
                # rssi = stats[1]
                # snr = stats[2]
                # sf = stats[3]
                #
                # msgData = ''
                # msgCrc = ''
                # if len(data_rx) >= 5:
                #     msg_data = data_rx[:-5]                  # remove the last 5 char data crc
                #     msg_crc = data_rx[-4:]                   # get the last 4 char
                #
                # # Calculate CRC
                # crc8 = utils.crc(msg_data)
                #
                # # Check CRC
                # crc_ok = True if crc8 == msg_crc.decode('utf-8') else False
                # # crc_ok = False
                # # if crc8 == msg_crc.decode('utf-8'):
                # #     crc_ok = True
                #
                # # fields_lorastats(LoraStats)
                #
                # # form csv row
                # msg = str(self.rxnb)
                # msg = msg + ',' + self.loramac.decode('utf8')
                # msg = msg + ',' + self.gps_buffer['time']
                # msg = msg + ',' + str(self.gps_buffer.lat)
                # msg = msg + ',' + str(self.gps_buffer.lon)
                # msg = msg + ',' + str(self.gps_buffer.alt)
                #
                # msg = msg + ',' + msg_data.decode('utf-8')
                # msg = msg + ',' + msg_crc.decode('utf-8')
                # msg = msg + ',' + str(crc8)
                # msg = msg + ',' + str(crc_ok)
                # msg = msg + ',' + str(time_stamp)
                # msg = msg + ',' + str(rssi)
                # msg = msg + ',' + str(snr)
                # msg = msg + ',' + str(sf)
                #
                # # # calc crc8 row
                # #crc8row = calc(msg.encode('utf-8'))
                # crc8row = utils.crc(msg.encode('utf-8'))
                #
                # # # add crc8row as last item
                # msg = msg + ',' + str(crc8row)
                #
                # # write csv and terminal
                # self.log_file.write(msg)
                # self.log_file.write('\n')
                # self.log_file.flush()
                #
                # print(msg)              # show in repl
                # self.rxnb += 1

    def start(self):
        # Initialize a LoRa sockect
        self.lora = LoRa(mode=LoRa.LORA)
        self.lora_sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
        self.lora_sock.setblocking(False)

        #self.tcp_alarm = Timer.Alarm(handler=lambda u: self._tcp_gps(), s=1, periodic=True)
        self.lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=self._lora_cb)

        # Start the TCP thread receiving GPS coordinates
        _thread.start_new_thread(self._tcp_thread, ())

    def stop(self):
        self.wlan.mode(network.WLAN.AP)
        self.tcp_sock.close()

        self.lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=None)
        self.lora_sock.close()
        #self.log_file.close()

        # Set end flag to terminate TCP thread
        self.end = True

    def _tcp_thread(self):
        while True:
            if self.end:
                _thread.exit()

            try:
                # Send request
                self.tcp_sock.send(b'?SHGPS.LOCATION;\r\n')
                # Get response
                rx_data = self.tcp_sock.recv(4096)

                # Release the lock in case of previous TCP error
                #self.lock.release()

                # Save last gps received to buffer
                with self.lock:
                    self.gps_buffer = json.loads(rx_data.decode('ascii'))
                    if not self.log_time:
                        self.log_time = self.gps_buffer['time']

            except socket.timeout:
                self.lock.locked()
            except OSError as e:
                if e.errno == errno.EAGAIN:
                    pass
                else:
                    self.lock.locked()
                    print(
                        "Error: Android 'ShareGPS' connection status should be 'Listening'"
                    )
                    print('Change mode and soft reboot Pycom device')
            except Exception:
                self.lock.locked()
                print("TCP recv Exception")
            time.sleep(0.5)
class NanoGateway:

    def __init__(self, id, frequency, datarate, ssid, password, server, port, ntp='pool.ntp.org', ntp_period=3600):
        self.id = id
        self.frequency = frequency
        self.datarate = datarate
        self.sf = self._dr_to_sf(datarate)
        self.ssid = ssid
        self.password = password
        self.server = server
        self.port = port
        self.ntp = ntp
        self.ntp_period = ntp_period

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

    def start(self):
        # Change WiFi to STA mode and connect
        self.wlan = WLAN(mode=WLAN.STA)
        self._connect_to_wifi()

        # Get a time Sync
        self.rtc = machine.RTC()
        self.rtc.ntp_sync(self.ntp, update_period=self.ntp_period)

        # Get the server IP and create an UDP socket
        self.server_ip = socket.getaddrinfo(self.server, self.port)[0][-1]
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # Push the first time immediatelly
        self._push_data(self._make_stat_packet())

        # Create the alarms
        self.stat_alarm = Timer.Alarm(handler=lambda t: self._push_data(self._make_stat_packet()), s=60, periodic=True)
        self.pull_alarm = Timer.Alarm(handler=lambda u: self._pull_data(), s=25, periodic=True)

        # Start the UDP receive thread
        _thread.start_new_thread(self._udp_thread, ())

        # Initialize LoRa in LORA mode
        self.lora = LoRa(mode=LoRa.LORA, frequency=self.frequency, bandwidth=LoRa.BW_125KHZ, sf=self.sf,
                         preamble=8, coding_rate=LoRa.CODING_4_5, tx_iq=True)
        # Create a raw LoRa socket
        self.lora_sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
        self.lora_sock.setblocking(False)
        self.lora_tx_done = False

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT), handler=self._lora_cb)

    def stop(self):
        # TODO: Check how to stop the NTP sync
        # TODO: Create a cancel method for the alarm
        # TODO: kill the UDP thread
        self.sock.close()

    def _connect_to_wifi(self):
        self.wlan.connect(self.ssid, auth=(None, self.password))
        while not self.wlan.isconnected():
            time.sleep(0.5)
        print("WiFi connected!")

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _sf_to_dr(self, sf):
        return self.datarate

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return json.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4], rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["datr"] = self._sf_to_dr(sf)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = float(snr)
        RX_PK["rxpk"][0]["data"] = binascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return json.dumps(RX_PK)

    def _push_data(self, data):
        token = os.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PUSH_DATA]) + binascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PUSH exception")

    def _pull_data(self):
        token = os.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_DATA]) + binascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PULL exception")

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = json.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_ACK]) + binascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PULL RSP ACK exception")

    def _lora_cb(self, lora):
        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            #self._push_data(self._make_node_packet(rx_data, self.rtc.now(), stats.rx_timestamp, stats.sfrx, stats.rssi, stats.snr))
            # Fix the "not joined yet" issue: https://forum.pycom.io/topic/1330/lopy-lorawan-gateway-with-an-st-lorawan-device/2
            self._push_data(self._make_node_packet(rx_data, self.rtc.now(), time.ticks_us(), stats.sfrx, stats.rssi, stats.snr))
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(mode=LoRa.LORA, frequency=self.frequency, bandwidth=LoRa.BW_125KHZ,
                      sf=self.sf, preamble=8, coding_rate=LoRa.CODING_4_5, tx_iq=True)

    def _send_down_link(self, data, tmst, datarate, frequency):
        self.lora.init(mode=LoRa.LORA, frequency=frequency, bandwidth=LoRa.BW_125KHZ,
                       sf=self._dr_to_sf(datarate), preamble=8, coding_rate=LoRa.CODING_4_5,
                       tx_iq=True)
        while time.ticks_us() < tmst:
            pass
        self.lora_sock.send(data)

    def _udp_thread(self):
        while True:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    print("Push ack")
                elif _type == PULL_ACK:
                    print("Pull ack")
                elif _type == PULL_RESP:
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = json.loads(data[4:])
                    tmst = tx_pk["txpk"]["tmst"]
                    t_us = tmst - time.ticks_us() - 5000
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = Timer.Alarm(handler=lambda x: self._send_down_link(binascii.a2b_base64(tx_pk["txpk"]["data"]),
                                                                                               tx_pk["txpk"]["tmst"] - 10, tx_pk["txpk"]["datr"],
                                                                                               int(tx_pk["txpk"]["freq"] * 1000000)), us=t_us)
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        print("Downlink timestamp error!, t_us:", t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    print("Pull rsp")
            except socket.timeout:
                pass
            except OSError as e:
                if e.errno == errno.EAGAIN:
                    pass
                else:
                    print("UDP recv OSError Exception")
            except Exception:
                print("UDP recv Exception")
            # Wait before trying to receive again
            time.sleep(0.025)
Example #19
0
class Zapette:
    def __init__(self, device_id, handler_rx=None, handler_tx=None):
        #.addHandler(ZapetteHandler("self"),2)
        l.debug("loraBlk init ...")
        self.device_id = bytes([device_id])
        self._user_handler_rx = handler_rx
        self._user_handler_tx = handler_tx
        n = uos.urandom(2)
        self.frame_cnt = n[0] + n[1] * 256
        self.lpp = zapetteLPP.LPP()
        # initialise LoRa in LORA mode
        self.lora = LoRa(
            mode=LoRa.LORA,
            region=LoRa.EU868,
            sf=12,
            tx_power=20,
            public=False,
            preamble=8,  #default
            bandwidth=LoRa.BW_125KHZ,
            coding_rate=LoRa.CODING_4_8)
        # create a raw LoRa socket
        self.lora_sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
        self.lora_sock.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED,
                                  True)  # selecting confirmed type of messages
        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT
                                    | LoRa.TX_PACKET_EVENT),
                           handler=self._lora_cb)
        self.frame_TX = []
        self.frame_ACK = None
        self.frame_ACK_stats = None
        self.tx_timeout = 4000
        self.tx_retries = 3

    def _lora_cb(self, lora):
        """
        LoRa radio events callback handler.
        """
        global RX, TX, lpp
        events = lora.events()
        stats = lora.stats()
        l.debug("event: {}".format(events))
        l.debug("stats: {}".format(stats))
        if events & LoRa.RX_PACKET_EVENT:
            rx_frame = self.lora_sock.recv(256)
            l.debug("rx : {}".format(ubinascii.hexlify(rx_frame)))
            #header
            device_from = rx_frame[0:1]
            device_to = rx_frame[1:2]
            frame_type = rx_frame[2:3]
            frame_cnt = rx_frame[3:5]
            payload = rx_frame[5:-1]
            crc = rx_frame[-1]

            l.debug(
                "device_from:{} ,device_to:{} ,frame_type:{}, frame_cnt:{}, crc:{}, payload:{}"
                .format(device_from, device_to, frame_type, frame_cnt, crc,
                        payload))
            if device_to == self.device_id:
                if frame_type == b'\x00':  #RX w/ ACK
                    l.debug("FRAME RX w/ ACK")
                    self._send_frame_ACK(device_from, frame_cnt, stats,
                                         b'\x01')
                    if self._user_handler_rx is not None:
                        lpp_rx = self.lpp.decode(payload)
                        if lpp_rx != None:
                            self._user_handler_rx(lpp_rx, {
                                'rssi': stats.rssi,
                                'snr': stats.snr
                            })
                    else:
                        l.debug("no Handler")
                elif frame_type == b'\x01':  #RX w/o ACK
                    l.debug("FRAME RX w/o ACK")
                    if self._user_handler_rx is not None:
                        lpp_rx = self.lpp.decode(payload)
                        if lpp_rx != None:
                            self._user_handler_rx(lpp_rx, {
                                'rssi': stats.rssi,
                                'snr': stats.snr
                            })
                    else:
                        l.debug("no Handler")
                elif frame_type == b'\x10':  #ACK
                    l.debug("FRAME ACK")
                    #tmp = struct.unpack("bhf",payload) Bug
                    status = payload[0]
                    rssi = struct.unpack("h", payload[1:3])[0]
                    snr = struct.unpack("f", payload[3:])[0]
                    self.frame_ACK = frame_cnt
                    self.frame_ACK_stats = {'rssi': rssi, 'snr': snr}
                    if self._user_handler_rx is not None:
                        self._user_handler_rx(None, {
                            'rssi': stats.rssi,
                            'snr': stats.snr
                        })
            else:
                l.debug("messages for: {}".format(device_to))

        elif events & LoRa.TX_PACKET_EVENT:
            l.debug("FRAME send")
        else:
            l.debug("???")

    def _send_frame_ACK(self, device_to, frame_cnt, stats, status):
        device_from = self.device_id
        device_to = device_to
        frame_type = b'\x10'
        frame_cnt = frame_cnt
        rssi = stats.rssi
        snr = stats.snr
        payload = status + struct.pack("h", rssi) + struct.pack("f", snr)
        crc = b'\x00'
        tx_frame = self.device_id + device_to + frame_type + frame_cnt + payload + crc
        l.debug("tx : {}".format(ubinascii.hexlify(tx_frame)))
        self.lora_sock.send(tx_frame)

    def send_frame_TX(self, device_to, msg, ack=True):
        device_from = self.device_id
        device_to = bytes([device_to])
        frame_type = b'\x00' if ack == True else b'\x01'
        frame_cnt = b'\x00\x01'
        payload = msg.get_buffer()
        crc = b'\x00'
        msg.reset()
        tx_frame = self.device_id + device_to + frame_type + frame_cnt + payload + crc
        l.debug("tx: {}, ack: {}".format(ubinascii.hexlify(tx_frame), ack))
        if ack == True:
            self.frame_ACK = None
            for i in range(0, self.tx_retries):
                self.lora_sock.send(tx_frame)
                ticks_sign = utime.ticks_diff(
                    1, 2)  # get the sign of ticks_diff, e.g. in init code.
                deadline = utime.ticks_add(utime.ticks_ms(), self.tx_timeout)
                if self._user_handler_tx is not None:
                    self._user_handler_tx(i)
                while (utime.ticks_diff(deadline, utime.ticks_ms()) *
                       ticks_sign) < 0:
                    if self.frame_ACK == frame_cnt:
                        l.debug("tx OK")
                        return self.frame_ACK_stats
                l.debug("tx NOK {}/{}".format(i + 1, self.tx_retries))
        else:
            self.lora_sock.send(tx_frame)
        return None
Example #20
0
class NanoGateway:
    """
    Nano gateway class, set up by default for use with TTN, but can be configured
    for any other network supporting the Semtech Packet Forwarder.
    Only required configuration is wifi_ssid and wifi_password which are used for
    connecting to the Internet.
    """

    def __init__(self, id, frequency, datarate, ssid, password, server, port, ntp_server='pool.ntp.org', ntp_period=3600):
        self.id = id
        self.server = server
        self.port = port

        self.frequency = frequency
        self.datarate = datarate

        self.ssid = ssid
        self.password = password

        self.ntp_server = ntp_server
        self.ntp_period = ntp_period

        self.server_ip = None

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.sf = self._dr_to_sf(self.datarate)
        self.bw = self._dr_to_bw(self.datarate)

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.wlan = None
        self.sock = None
        self.udp_stop = False
        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

        self.rtc = machine.RTC()

    def start(self):
        """
        Starts the LoRaWAN nano gateway.
        """

        self._log('Starting LoRaWAN nano gateway with id: {}', self.id)

        # setup WiFi as a station and connect
        self.wlan = WLAN(mode=WLAN.STA)
        self._connect_to_wifi()

        # get a time sync
        self._log('Syncing time with {} ...', self.ntp_server)
        self.rtc.ntp_sync(self.ntp_server, update_period=self.ntp_period)
        while not self.rtc.synced():
            utime.sleep_ms(50)
        self._log("RTC NTP sync complete")

        # get the server IP and create an UDP socket
        self.server_ip = usocket.getaddrinfo(self.server, self.port)[0][-1]
        self._log('Opening UDP socket to {} ({}) port {}...', self.server, self.server_ip[0], self.server_ip[1])
        self.sock = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM, usocket.IPPROTO_UDP)
        self.sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # push the first time immediatelly
        self._push_data(self._make_stat_packet())

        # create the alarms
        self.stat_alarm = Timer.Alarm(handler=lambda t: self._push_data(self._make_stat_packet()), s=60, periodic=True)
        self.pull_alarm = Timer.Alarm(handler=lambda u: self._pull_data(), s=25, periodic=True)

        # start the UDP receive thread
        self.udp_stop = False
        _thread.start_new_thread(self._udp_thread, ())

        # initialize the LoRa radio in LORA mode
        self._log('Setting up the LoRa radio at {:.1f} Mhz using {}', self._freq_to_float(self.frequency), self.datarate)
        self.lora = LoRa(
            mode=LoRa.LORA,
            frequency=self.frequency,
            bandwidth=self.bw,
            sf=self.sf,
            preamble=8,
            coding_rate=LoRa.CODING_4_5,
            tx_iq=True
        )

        # create a raw LoRa socket
        self.lora_sock = usocket.socket(usocket.AF_LORA, usocket.SOCK_RAW)
        self.lora_sock.setblocking(False)
        self.lora_tx_done = False

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT), handler=self._lora_cb)
        self._log('LoRaWAN nano gateway online')

    def stop(self):
        """
        Stops the LoRaWAN nano gateway.
        """

        self._log('Stopping...')

        # send the LoRa radio to sleep
        self.lora.callback(trigger=None, handler=None)
        self.lora.power_mode(LoRa.SLEEP)

        # stop the NTP sync
        self.rtc.ntp_sync(None)

        # cancel all the alarms
        self.stat_alarm.cancel()
        self.pull_alarm.cancel()

        # signal the UDP thread to stop
        self.udp_stop = True
        while self.udp_stop:
            utime.sleep_ms(50)

        # disable WLAN
        self.wlan.disconnect()
        self.wlan.deinit()

    def _connect_to_wifi(self):
        self.wlan.connect(self.ssid, auth=(None, self.password))
        while not self.wlan.isconnected():
            utime.sleep_ms(50)
        self._log('WiFi connected to: {}', self.ssid)

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _dr_to_bw(self, dr):
        bw = dr[-5:]
        if bw == 'BW125':
            return LoRa.BW_125KHZ
        elif bw == 'BW250':
            return LoRa.BW_250KHZ
        else:
            return LoRa.BW_500KHZ

    def _sf_bw_to_dr(self, sf, bw):
        dr = 'SF' + str(sf)
        if bw == LoRa.BW_125KHZ:
            return dr + 'BW125'
        elif bw == LoRa.BW_250KHZ:
            return dr + 'BW250'
        else:
            return dr + 'BW500'

    def _lora_cb(self, lora):
        """
        LoRa radio events callback handler.
        """

        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            packet = self._make_node_packet(rx_data, self.rtc.now(), stats.rx_timestamp, stats.sfrx, self.bw, stats.rssi, stats.snr)
            self._push_data(packet)
            self._log('Received packet: {}', packet)
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(
                mode=LoRa.LORA,
                frequency=self.frequency,
                bandwidth=self.bw,
                sf=self.sf,
                preamble=8,
                coding_rate=LoRa.CODING_4_5,
                tx_iq=True
                )

    def _freq_to_float(self, frequency):
        """
        MicroPython has some inprecision when doing large float division.
        To counter this, this method first does integer division until we
        reach the decimal breaking point. This doesn't completely elimate
        the issue in all cases, but it does help for a number of commonly
        used frequencies.
        """

        divider = 6
        while divider > 0 and frequency % 10 == 0:
            frequency = frequency // 10
            divider -= 1
        if divider > 0:
            frequency = frequency / (10 ** divider)
        return frequency

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return ujson.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, bw, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4], rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["freq"] = self._freq_to_float(self.frequency)
        RX_PK["rxpk"][0]["datr"] = self._sf_bw_to_dr(sf, bw)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = snr
        RX_PK["rxpk"][0]["data"] = ubinascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return ujson.dumps(RX_PK)

    def _push_data(self, data):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PUSH_DATA]) + ubinascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to push uplink packet to server: {}', ex)

    def _pull_data(self):
        token = uos.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_DATA]) + ubinascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('Failed to pull downlink packets from server: {}', ex)

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = ujson.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes([PULL_ACK]) + ubinascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception as ex:
                self._log('PULL RSP ACK exception: {}', ex)

    def _send_down_link(self, data, tmst, datarate, frequency):
        """
        Transmits a downlink message over LoRa.
        """

        self.lora.init(
            mode=LoRa.LORA,
            frequency=frequency,
            bandwidth=self._dr_to_bw(datarate),
            sf=self._dr_to_sf(datarate),
            preamble=8,
            coding_rate=LoRa.CODING_4_5,
            tx_iq=True
            )
        while utime.ticks_us() < tmst:
            pass
        self.lora_sock.send(data)
        self._log(
            'Sent downlink packet scheduled on {:.3f}, at {:.1f} Mhz using {}: {}',
            tmst / 1000000,
            self._freq_to_float(frequency),
            datarate,
            data
        )

    def _udp_thread(self):
        """
        UDP thread, reads data from the server and handles it.
        """

        while not self.udp_stop:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    self._log("Push ack")
                elif _type == PULL_ACK:
                    self._log("Pull ack")
                elif _type == PULL_RESP:
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = ujson.loads(data[4:])
                    tmst = tx_pk["txpk"]["tmst"]
                    t_us = tmst - utime.ticks_us() - 12500
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = Timer.Alarm(
                            handler=lambda x: self._send_down_link(
                                ubinascii.a2b_base64(tx_pk["txpk"]["data"]),
                                tx_pk["txpk"]["tmst"] - 50, tx_pk["txpk"]["datr"],
                                int(tx_pk["txpk"]["freq"] * 1000000)
                            ), 
                            us=t_us
                        )
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        self._log('Downlink timestamp error!, t_us: {}', t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    self._log("Pull rsp")
            except usocket.timeout:
                pass
            except OSError as ex:
                if ex.errno != errno.EAGAIN:
                    self._log('UDP recv OSError Exception: {}', ex)
            except Exception as ex:
                self._log('UDP recv Exception: {}', ex)

            # wait before trying to receive again
            utime.sleep_ms(UDP_THREAD_CYCLE_MS)

        # we are to close the socket
        self.sock.close()
        self.udp_stop = False
        self._log('UDP thread stopped')

    def _log(self, message, *args):
        """
        Outputs a log message to stdout.
        """

        print('[{:>10.3f}] {}'.format(
            utime.ticks_ms() / 1000,
            str(message).format(*args)
            ))
Example #21
0
class LoRaWAN:
    def __init__(self, logger):
        """
        Connects to LoRaWAN using OTAA and sets up a ring buffer for storing messages.
        :param logger: status logger
        :type logger: LoggerFactory object
        """

        self.logger = logger
        self.message_limit = int(
            float(config.get_config("fair_access")) /
            (float(config.get_config("air_time")) / 1000))
        self.transmission_date = config.get_config(
            "transmission_date")  # last date when lora was transmitting
        today = time.gmtime()
        date = str(today[0]) + str(today[1]) + str(today[2])
        if self.transmission_date == date:  # if device was last transmitting today
            self.message_count = config.get_config(
                "message_count")  # get number of messages sent today
        else:
            self.message_count = 0  # if device was last transmitting a day or more ago, reset message_count for the day
            self.transmission_date = date
            config.save_config({
                "message_count": self.message_count,
                "transmission_date": date
            })

        regions = {
            "Europe": LoRa.EU868,
            "Asia": LoRa.AS923,
            "Australia": LoRa.AU915,
            "United States": LoRa.US915
        }
        region = regions[config.get_config("region")]

        self.lora = LoRa(mode=LoRa.LORAWAN, region=region, adr=True)

        # create an OTAA authentication parameters
        app_eui = ubinascii.unhexlify(config.get_config("application_eui"))
        app_key = ubinascii.unhexlify(config.get_config("app_key"))

        # join a network using OTAA (Over the Air Activation)
        self.lora.join(activation=LoRa.OTAA,
                       auth=(app_eui, app_key),
                       timeout=0)

        # create a LoRa socket
        self.lora_socket = socket.socket(socket.AF_LORA, socket.SOCK_RAW)

        # request acknowledgment of data sent
        # self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, True)

        # do not request acknowledgment of data sent
        self.lora_socket.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED,
                                    False)

        # sets timeout for sending data
        self.lora_socket.settimeout(
            int(config.get_config("lora_timeout")) * 1000)

        # set up callback for receiving downlink messages
        self.lora.callback(trigger=LoRa.RX_PACKET_EVENT,
                           handler=self.lora_recv)

        # initialises circular lora stack to back up data up to about 22.5 days depending on the length of the month
        self.lora_buffer = RingBuffer(self.logger, s.processing_path,
                                      s.lora_file_name,
                                      31 * self.message_limit, 100)

        try:  # this fails if the buffer is empty
            self.check_date()  # remove messages that are over a month old
        except Exception as e:
            pass

    def lora_recv(self, arg):
        """
        Callback for receiving packets through LoRaWAN. Decodes messages to commands for updating over WiFi.
        Requires a dummy argument.
        """

        payload = self.lora_socket.recv(600)  # receive bytes message
        self.logger.info("Lora message received")
        msg = payload.decode()  # convert to string

        try:
            if msg == "0":  # reboot device
                self.logger.info("Reset triggered over LoRa")
                self.logger.info("Rebooting...")
                machine.reset()
            elif msg == "1":  # start software update
                self.logger.info("Software update triggered over LoRa")
                config.save_config({"update": True})
                machine.reset()
            else:
                split_msg = msg.split(":")
                if split_msg[0] == "2":  # update wifi credentials
                    self.logger.info("WiFi credentials updated over LoRa")
                    config.save_config({
                        "SSID": split_msg[1],
                        "wifi_password": split_msg[2]
                    })
                elif split_msg[
                        0] == "3":  # update wifi credentials and start software update
                    self.logger.info("WiFi credentials updated over LoRa")
                    config.save_config({
                        "SSID": split_msg[1],
                        "wifi_password": split_msg[2]
                    })
                    self.logger.info("Software update triggered over LoRa")
                    config.save_config({"update": True})
                    machine.reset()
                else:
                    self.logger.error("Unknown command received over LoRa")
        except Exception as e:
            self.logger.exception(
                "Failed to interpret message received over LoRa")

    def lora_send(self):
        """Lora send method to run as a thread. Checks if messages are up to date in the lora buffer, pops the one on
        top of the stack, encodes it to a message and sends it to the right port.
        Takes two dummy arguments required by the threading library"""

        if lora_lock.locked():
            self.logger.debug("Waiting for other lora thread to finish")
        with lora_lock:
            self.logger.debug("LoRa thread started")

            try:
                self.check_date()  # remove messages that are over a month old

                if self.lora.has_joined():
                    self.logger.debug("LoRa connected")
                else:
                    raise Exception("LoRa is not connected")

                if s.lora_file_name not in os.listdir(s.root_path +
                                                      s.processing):
                    raise Exception('LoRa - File: {} does not exist'.format(
                        s.lora_file_name))
                else:
                    port, payload = self.get_sending_details()

                    self.lora_socket.bind(
                        port)  # bind to port to decode at backend
                    self.lora_socket.send(
                        payload)  # send payload to the connected socket
                    self.logger.debug("LoRa - sent payload")

                    self.message_count += 1  # increment number of files sent over LoRa today

                    config.save_config({"message_count": self.message_count
                                        })  # save number of messages today

                    # remove message sent
                    self.lora_buffer.remove_head()

            except Exception:
                self.logger.exception("Sending payload over LoRaWAN failed")
                blink_led((0x550000, 0.4, True))

    def get_sending_details(self):
        """
        Gets message sitting on top of the lora stack, and constructs payload according to its format
        :return: port, payload
        :rtype: int, bytes
        """

        buffer_line = self.lora_buffer.read()
        buffer_lst = buffer_line.split(
            ',')  # convert string to a list of strings

        # get structure and port from format
        fmt = buffer_lst[2]  # format is third item in the list
        fmt_dict = {
            "TPP": s.TPP,
            "TP": s.TP,
            "PP": s.PP,
            "P": s.P,
            "T": s.T,
            "G": s.G
        }
        port_struct_dict = fmt_dict[
            fmt]  # get dictionary corresponding to the format
        port = port_struct_dict["port"]  # get port corresponding to the format
        structure = port_struct_dict[
            "structure"]  # get structure corresponding to the format

        # cast message according to format to form valid payload
        lst_message = buffer_lst[3:]  # chop year, month and format off
        cast_lst_message = []
        for i in range(
            (len(structure) -
             1)):  # iterate for length of structure having '<' stripped
            if structure[i +
                         1] == 'f':  # iterate through structure ignoring '<'
                cast_lst_message.append(float(
                    lst_message[i]))  # cast to float if structure is 'f'
            else:
                cast_lst_message.append(int(
                    lst_message[i]))  # cast to int otherwise

        # pack payload
        self.logger.debug("Sending over LoRa: " + str(cast_lst_message))
        payload = struct.pack(
            structure, *cast_lst_message
        )  # define payload with given structure and list of averages

        return port, payload

    # removes messages from end of lora stack until they are all within a month
    def check_date(self):
        """
        Checks recursively if the message on the bottom of the stack is within a month
        """

        buffer_line = self.lora_buffer.read(
            read_tail=True)  # read the tail of the lora_buffer
        buffer_lst = buffer_line.split(
            ',')  # convert string to a list of strings
        time_now = time.gmtime()  # get current date

        # get year, month and minutes of the month now
        year_now, month_now, minutes_now = time_now[0], time_now[
            1], minutes_of_the_month()

        # get year, month and minutes of the month of the last message in the lora_buffer
        year_then, month_then, minutes_then = int(buffer_lst[0]) + 2000, int(
            buffer_lst[1]), int(buffer_lst[4])

        # logic to decide if message is older than a month
        if year_then < year_now or month_then < month_now:
            if (month_then + 1
                    == month_now) or (month_then == 12 and month_now == 1
                                      and year_then + 1 == year_now):
                if minutes_then < minutes_now + 24 * 60:
                    self.lora_buffer.remove_tail()  # remove message
                    self.check_date()  # call recursively
            else:
                self.lora_buffer.remove_tail()  # remove message
                self.check_date()  # call recursively
Example #22
0
    lora.add_channel(4, frequency=867300000, dr_min=0, dr_max=5)
    lora.add_channel(5, frequency=867500000, dr_min=0, dr_max=5)
    lora.add_channel(6, frequency=867700000, dr_min=0, dr_max=5)
    lora.add_channel(7, frequency=867900000, dr_min=0, dr_max=5)

# create a LoRa socket
s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)

# set the LoRaWAN data rate
s.setsockopt(socket.SOL_LORA, socket.SO_DR, LORA_DR)

# settings for ACK after TX
s.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, LORA_ACK)

# install callback handler for TX/RX events
lora.callback(trigger=(LoRa.RX_PACKET_EVENT|LoRa.TX_PACKET_EVENT), handler=event_handler)

# send data every 30 seconds
counter = 0
while True and lora.has_joined():
    counter += 1
    RX_EVENT = False
    print("Sending uplink packet {} with SF{}{}...".format(
        counter, 12-LORA_DR, ' asking for ACK' if LORA_ACK else ''), end="")
    try:
        s.setblocking(True)
        s.settimeout(10)
        flash_led(LED_GREEN, 0.1, 0.3, 2)
        s.send(bytes([counter]))
        print("OK.")
    except socket.timeout:
Example #23
0
def lora_cb_handler(lora):
    global s
    print(type(lora))
    try:
        events = lora.events()
        if events & LoRa.TX_PACKET_EVENT:
            print("Packet sent")
        if events & LoRa.RX_PACKET_EVENT:
            print("Packet received")
            print(s.recv(64))
    except Exception:
        print('Exception')


cb = lora.callback(handler=lora_cb_handler,
                   trigger=LoRa.TX_PACKET_EVENT | LoRa.RX_PACKET_EVENT)
s.setblocking(True)
for i in range(2):
    print(s.send("Sending pk #%d" % i))
    time.sleep(0.5)

lst = (lora, s)

cb = lora.callback(handler=lora_cb_handler,
                   trigger=LoRa.TX_PACKET_EVENT | LoRa.RX_PACKET_EVENT,
                   arg=lst)
s.setblocking(True)
for i in range(2):
    print(s.send("Sending pk #%d" % i))
    time.sleep(0.5)
Example #24
0
class NanoGateway:
    def __init__(self,
                 id,
                 frequency,
                 datarate,
                 ssid,
                 password,
                 server,
                 port,
                 ntp='pool.ntp.org',
                 ntp_period=3600):
        self.id = id
        self.frequency = frequency
        self.sf = self._dr_to_sf(datarate)
        self.ssid = ssid
        self.password = password
        self.server = server
        self.port = port
        self.ntp = ntp
        self.ntp_period = ntp_period

        self.rxnb = 0
        self.rxok = 0
        self.rxfw = 0
        self.dwnb = 0
        self.txnb = 0

        self.stat_alarm = None
        self.pull_alarm = None
        self.uplink_alarm = None

        self.udp_lock = _thread.allocate_lock()

        self.lora = None
        self.lora_sock = None

    def start(self):
        # Change WiFi to STA mode and connect
        self.wlan = WLAN(mode=WLAN.STA)
        self._connect_to_wifi()

        # Get a time Sync
        self.rtc = machine.RTC()
        self.rtc.ntp_sync(self.ntp, update_period=self.ntp_period)

        # Get the server IP and create an UDP socket
        self.server_ip = socket.getaddrinfo(self.server, self.port)[0][-1]
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
                                  socket.IPPROTO_UDP)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.setblocking(False)

        # Push the first time immediatelly
        self._push_data(self._make_stat_packet())

        # Create the alarms
        self.stat_alarm = Timer.Alarm(
            handler=lambda t: self._push_data(self._make_stat_packet()),
            s=60,
            periodic=True)
        self.pull_alarm = Timer.Alarm(handler=lambda u: self._pull_data(),
                                      s=25,
                                      periodic=True)

        # Start the UDP receive thread
        _thread.start_new_thread(self._udp_thread, ())

        # Initialize LoRa in LORA mode
        self.lora = LoRa(mode=LoRa.LORA,
                         frequency=self.frequency,
                         bandwidth=LoRa.BW_125KHZ,
                         sf=self.sf,
                         preamble=8,
                         coding_rate=LoRa.CODING_4_5,
                         tx_iq=True)
        # Create a raw LoRa socket
        self.lora_sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
        self.lora_sock.setblocking(False)
        self.lora_tx_done = False

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT
                                    | LoRa.TX_PACKET_EVENT),
                           handler=self._lora_cb)

    def stop(self):
        # TODO: Check how to stop the NTP sync
        # TODO: Create a cancel method for the alarm
        # TODO: kill the UDP thread
        self.sock.close()

    def _connect_to_wifi(self):
        self.wlan.connect(self.ssid, auth=(None, self.password))
        while not self.wlan.isconnected():
            time.sleep(0.5)
        print("WiFi connected!")

    def _dr_to_sf(self, dr):
        sf = dr[2:4]
        if sf[1] not in '0123456789':
            sf = sf[:1]
        return int(sf)

    def _sf_to_dr(self, sf):
        return "SF7BW125"

    def _make_stat_packet(self):
        now = self.rtc.now()
        STAT_PK["stat"]["time"] = "%d-%02d-%02d %02d:%02d:%02d GMT" % (
            now[0], now[1], now[2], now[3], now[4], now[5])
        STAT_PK["stat"]["rxnb"] = self.rxnb
        STAT_PK["stat"]["rxok"] = self.rxok
        STAT_PK["stat"]["rxfw"] = self.rxfw
        STAT_PK["stat"]["dwnb"] = self.dwnb
        STAT_PK["stat"]["txnb"] = self.txnb
        return json.dumps(STAT_PK)

    def _make_node_packet(self, rx_data, rx_time, tmst, sf, rssi, snr):
        RX_PK["rxpk"][0]["time"] = "%d-%02d-%02dT%02d:%02d:%02d.%dZ" % (
            rx_time[0], rx_time[1], rx_time[2], rx_time[3], rx_time[4],
            rx_time[5], rx_time[6])
        RX_PK["rxpk"][0]["tmst"] = tmst
        RX_PK["rxpk"][0]["datr"] = self._sf_to_dr(sf)
        RX_PK["rxpk"][0]["rssi"] = rssi
        RX_PK["rxpk"][0]["lsnr"] = float(snr)
        RX_PK["rxpk"][0]["data"] = binascii.b2a_base64(rx_data)[:-1]
        RX_PK["rxpk"][0]["size"] = len(rx_data)
        return json.dumps(RX_PK)

    def _push_data(self, data):
        token = os.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PUSH_DATA]) + binascii.unhexlify(self.id) + data
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PUSH exception")

    def _pull_data(self):
        token = os.urandom(2)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_DATA]) + binascii.unhexlify(self.id)
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PULL exception")

    def _ack_pull_rsp(self, token, error):
        TX_ACK_PK["txpk_ack"]["error"] = error
        resp = json.dumps(TX_ACK_PK)
        packet = bytes([PROTOCOL_VERSION]) + token + bytes(
            [PULL_ACK]) + binascii.unhexlify(self.id) + resp
        with self.udp_lock:
            try:
                self.sock.sendto(packet, self.server_ip)
            except Exception:
                print("PULL RSP ACK exception")

    def _lora_cb(self, lora):
        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            self.rxnb += 1
            self.rxok += 1
            rx_data = self.lora_sock.recv(256)
            stats = lora.stats()
            self._push_data(
                self._make_node_packet(rx_data, self.rtc.now(),
                                       stats.timestamp, stats.sf, stats.rssi,
                                       stats.snr))
            self.rxfw += 1
        if events & LoRa.TX_PACKET_EVENT:
            self.txnb += 1
            lora.init(mode=LoRa.LORA,
                      frequency=self.frequency,
                      bandwidth=LoRa.BW_125KHZ,
                      sf=self.sf,
                      preamble=8,
                      coding_rate=LoRa.CODING_4_5,
                      tx_iq=True)

    def _send_down_link(self, data, tmst, datarate, frequency):
        self.lora.init(mode=LoRa.LORA,
                       frequency=frequency,
                       bandwidth=LoRa.BW_125KHZ,
                       sf=self._dr_to_sf(datarate),
                       preamble=8,
                       coding_rate=LoRa.CODING_4_5,
                       tx_iq=True)
        while time.ticks_us() < tmst:
            pass
        self.lora_sock.send(data)

    def _udp_thread(self):
        while True:
            try:
                data, src = self.sock.recvfrom(1024)
                _token = data[1:3]
                _type = data[3]
                if _type == PUSH_ACK:
                    print("Push ack")
                elif _type == PULL_ACK:
                    print("Pull ack")
                elif _type == PULL_RESP:
                    self.dwnb += 1
                    ack_error = TX_ERR_NONE
                    tx_pk = json.loads(data[4:])
                    tmst = tx_pk["txpk"]["tmst"]
                    t_us = tmst - time.ticks_us() - 5000
                    if t_us < 0:
                        t_us += 0xFFFFFFFF
                    if t_us < 20000000:
                        self.uplink_alarm = Timer.Alarm(
                            handler=lambda x: self._send_down_link(
                                binascii.a2b_base64(tx_pk["txpk"]["data"]),
                                tx_pk["txpk"]["tmst"] - 10, tx_pk["txpk"][
                                    "datr"],
                                int(tx_pk["txpk"]["freq"] * 1000000)),
                            us=t_us)
                    else:
                        ack_error = TX_ERR_TOO_LATE
                        print("Downlink timestamp error!, t_us:", t_us)
                    self._ack_pull_rsp(_token, ack_error)
                    print("Pull rsp")
            except socket.timeout:
                pass
            except OSError as e:
                if e.errno == errno.EAGAIN:
                    pass
                else:
                    print("UDP recv OSError Exception")
            except Exception:
                print("UDP recv Exception")
            # Wait before trying to receive again
            time.sleep(0.025)
Example #25
0
class LoraNet:
    def __init__(self,
                 frequency,
                 dr,
                 region,
                 device_class=LoRa.CLASS_C,
                 activation=LoRa.OTAA,
                 auth=None):
        self.frequency = frequency
        self.dr = dr
        self.region = region
        self.device_class = device_class
        self.activation = activation
        self.auth = auth
        self.sock = None
        self._exit = False
        self.s_lock = _thread.allocate_lock()
        self.lora = LoRa(mode=LoRa.LORAWAN,
                         region=self.region,
                         device_class=self.device_class)

        self._msg_queue = []
        self.q_lock = _thread.allocate_lock()
        self._process_ota_msg = None

    def stop(self):
        self._exit = True

    def init(self, process_msg_callback):
        self._process_ota_msg = process_msg_callback

    def receive_callback(self, lora):
        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            rx, port = self.sock.recvfrom(256)
            if rx:
                if '$OTA' in rx:
                    print("OTA msg received: {}".format(rx))
                    self._process_ota_msg(rx.decode())
                else:
                    self.q_lock.acquire()
                    self._msg_queue.append(rx)
                    self.q_lock.release()

    def connect(self):
        if self.activation != LoRa.OTAA and self.activation != LoRa.ABP:
            raise ValueError("Invalid Lora activation method")
        if len(self.auth) < 3:
            raise ValueError("Invalid authentication parameters")

        self.lora.callback(trigger=LoRa.RX_PACKET_EVENT,
                           handler=self.receive_callback)

        # set the 3 default channels to the same frequency
        self.lora.add_channel(0, frequency=self.frequency, dr_min=0, dr_max=5)
        self.lora.add_channel(1, frequency=self.frequency, dr_min=0, dr_max=5)
        self.lora.add_channel(2, frequency=self.frequency, dr_min=0, dr_max=5)

        # remove all the non-default channels
        for i in range(3, 16):
            self.lora.remove_channel(i)

        # authenticate with abp or ota
        if self.activation == LoRa.OTAA:
            self._authenticate_otaa(self.auth)
        else:
            self._authenticate_abp(self.auth)

        # create socket to server
        self._create_socket()

    def _authenticate_otaa(self, auth_params):

        # create an OTAA authentication params
        self.dev_eui = binascii.unhexlify(auth_params[0])
        self.app_eui = binascii.unhexlify(auth_params[1])
        self.app_key = binascii.unhexlify(auth_params[2])

        self.lora.join(activation=LoRa.OTAA,
                       auth=(self.dev_eui, self.app_eui, self.app_key),
                       timeout=0,
                       dr=self.dr)

        while not self.lora.has_joined():
            time.sleep(2.5)
            print('Not joined yet...')

    def has_joined(self):
        return self.lora.has_joined()

    def _authenticate_abp(self, auth_params):
        # create an ABP authentication params
        self.dev_addr = struct.unpack(">l",
                                      binascii.unhexlify(auth_params[0]))[0]
        self.nwk_swkey = binascii.unhexlify(auth_params[1])
        self.app_swkey = binascii.unhexlify(auth_params[2])

        self.lora.join(activation=LoRa.ABP,
                       auth=(self.dev_addr, self.nwk_swkey, self.app_swkey))

    def _create_socket(self):

        # create a LoRa socket
        self.sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW)

        # set the LoRaWAN data rate
        self.sock.setsockopt(socket.SOL_LORA, socket.SO_DR, self.dr)

        # make the socket non blocking
        self.sock.setblocking(False)

        time.sleep(2)

    def send(self, packet):
        with self.s_lock:
            self.sock.send(packet)

    def receive(self, bufsize):
        with self.q_lock:
            if len(self._msg_queue) > 0:
                return self._msg_queue.pop(0)
        return ''

    def get_dev_eui(self):
        return binascii.hexlify(self.lora.mac()).decode('ascii')

    def change_to_multicast_mode(self, mcAuth):
        print('Start listening for firmware updates ...........')

        if self.device_class != LoRa.CLASS_C:
            self.lora = LoRa(mode=LoRa.LORAWAN,
                             region=self.region,
                             device_class=LoRa.CLASS_C)
            self.connect()

        mcAddr = struct.unpack(">l", binascii.unhexlify(mcAuth[0]))[0]
        mcNwkKey = binascii.unhexlify(mcAuth[1])
        mcAppKey = binascii.unhexlify(mcAuth[2])

        self.lora.join_multicast_group(mcAddr, mcNwkKey, mcAppKey)
Example #26
0
print("Joined LoRa Network!")

# Print LoRa Stats
lora.stats()


# Call back for T/RX Messages
def lora_cb(lora):
    events = lora.events()
    if events & LoRa.RX_PACKET_EVENT:
        print('Lora packet received')
    if events & LoRa.TX_PACKET_EVENT:
        print('Lora packet sent')


lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT),
              handler=lora_cb)

# create a LoRa socket
s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)

# set the LoRaWAN data rate
#s.setsockopt(socket.SOL_LORA, socket.SO_DR, 5)

# selecting confirmed type of messages
s.setsockopt(socket.SOL_LORA, socket.SO_CONFIRMED, True)

# make the socket non-blocking
s.setblocking(False)

# send some data
while True:
Example #27
0
class LoraController:
    def __init__(self, options, logger, eventLog, ledController):
        self.options = options
        self.logger = logger
        self.eventLog = eventLog
        self.led = ledController

        self.lora = LoRa(mode=LoRa.LORA, power_mode=LoRa.SLEEP)
        self.tx_runner = None  # thread which sends events over lora
        self.lastJoin = 0  # when did we join the lora network
        self.isJoinLogged = False  # did we log the initial LORA join
        self.lastEventId = 0  # last sent event id
        self.sendLock = _thread.allocate_lock()
        self.socketLock = _thread.allocate_lock()
        self.isAckingCounter = 0
        self.noDownlinkCounter = 0
        self.lastUplinkTime = 0
        self.isAcking = False

    # logging
    def log(self, *text):
        self.logger.log("LORA", *text)

    # start lora connectivity
    def start(self):
        # setup lorawan
        self.lora = LoRa(mode=LoRa.LORAWAN,
                         region=LoRa.EU868,
                         device_class=LoRa.CLASS_A,
                         tx_retries=3,
                         adr=True,
                         sf=12)
        self.lora.nvram_restore()

        self.lora.callback(trigger=(LoRa.RX_PACKET_EVENT | LoRa.TX_PACKET_EVENT
                                    | LoRa.TX_FAILED_EVENT),
                           handler=self.lora_callback)
        self.log('Lora DevEUI is', self.getDeviceEUI())
        self.log('Lora AppEUI is', self.options['lora_app_eui'])

        if len(self.options['lora_app_eui']) != 16:
            self.log("ERROR", "Setting 'lora_app_eui' is invalid:",
                     self.options['lora_app_eui'])
            return

        # issue join
        if self.options['lora_mode'] == "abp":
            self.join()
        elif self.lora.has_joined():
            self.log("Lora network is already joined, re-joining anyway")
        else:
            self.join()

    def lora_callback(self, lora):
        events = lora.events()
        if events & LoRa.TX_FAILED_EVENT:
            self.log('Lora TX FAILED')

    # determines the LORA MAC address (string)
    def getDeviceEUI(self):
        return ubinascii.hexlify(self.lora.mac()).decode('ascii').upper()

    # joins the lora network via OTAA
    def joinOTAA(self):
        app_eui = ubinascii.unhexlify(self.options['lora_app_eui'])
        app_key = ubinascii.unhexlify(self.options['lora_app_key'])

        self.lora.join(activation=LoRa.OTAA,
                       auth=(app_eui, app_key),
                       timeout=0)
        self.log("Joining via OTAA")
        self.lastJoin = time.time()

    # joins the lora network via ABP
    def joinABP(self):
        net_key = ubinascii.unhexlify(self.options['lora_net_key'])
        app_key = ubinascii.unhexlify(self.options['lora_app_key'])
        # note: TTN seems to want the reverse bytes of this address
        device_address = ubinascii.unhexlify(self.options['lora_dev_adr'])

        self.lora.join(activation=LoRa.ABP,
                       auth=(device_address, net_key, app_key))
        self.log("Joining via ABP with device address", device_address)
        self.lastJoin = time.time()

    # joins the lora network via ABP or OTAA
    def join(self):
        if self.options['lora_mode'] == "abp":
            self.joinABP()
        else:
            self.joinOTAA()

    def hasJoined(self):
        return self.lora.has_joined()

    def stats(self):
        return self.lora.stats()

    def makePayload(self, event):
        payload = None
        command = event['Command']
        idbytes = event['ID'].to_bytes(2, 'little')
        event_ts = event['Time']
        try:
            if command == eventlog.CMD_TAG_DETECTED:
                # Tag with 4-Byte UID detected
                # <0x01> <Event ID 0..1> <Timestamp 0..3> <UID 0..3/6/9>
                timeBytes = event_ts.to_bytes(4, 'little')
                uid = event['Data'][0:10]

                # remove trailing 0x00
                uid_size = 10
                for i in range(uid_size - 1, 3, -1):
                    if uid[i] != 0x00:
                        break
                    uid_size = uid_size - 1
                uid = uid[:uid_size]

                payload = bytes([0x01]) + idbytes + timeBytes + uid
                uidText = ubinascii.hexlify(uid).decode()
                self.log("CMD 0x01 [NFC_DETECTED] SEQ#", event['ID'],
                         ". uid =", uidText, ", ts =", event_ts)

            if command == eventlog.CMD_TIME_REQUEST2:
                # ask backend for current time (new)
                # <0x04> <ID 0..1> <Our Time 0..3>
                mytime = time.time().to_bytes(4, 'little')
                payload = bytes([command]) + idbytes + mytime
                self.log("CMD 0x04 [TIME_REQUEST] ID#",
                         event['ID'], ". our_time =", time.time(),
                         utime.gmtime(time.time()))

            if command == eventlog.CMD_TIME_CHANGED:
                # <0x05> <Event ID 0..1> <Our Time 0..3> <Old Time 0..3>
                mytime = event_ts.to_bytes(4, 'little')
                oldTime = event['Data'][0:4]
                payload = bytes([eventlog.CMD_TIME_CHANGED
                                 ]) + idbytes + mytime + oldTime
                self.log("CMD 0x05 [TIME_CHANGED] SEQ#",
                         event['ID'], ". our_time =", event_ts,
                         utime.gmtime(event_ts), ", old_time =", oldTime)

        except Exception as e:
            self.log("ERROR: Unable to prepare LORA payload:", e.args[0], e)
        return payload

    # attempts to send the given event
    def sendEvent(self, event):
        with self.sendLock:
            eventId = event['ID']
            command = event['Command']
            self.log("Preparing to send CMD =", command, ", SEQ_NO =", eventId)
            if self.lastEventId > 0 and eventId > self.lastEventId + 1:
                self.log("ERROR", "Event IDs are not in sequence - last:",
                         self.lastEventId, ", current:", eventId)
            self.lastEventId = eventId
            # prepare lora payload for supported event log entries
            payload = self.makePayload(event)
            if payload == None:
                self.log(
                    "WARN: Event payload is None and therefore ignored for lora transmission"
                )
                return True
            # send payload
            return self.sendAndHandleResponse(payload)

    # sends the payload and handles the optional response
    def sendAndHandleResponse(self, payload):
        if not self.hasJoined():
            self.log("ERROR", "Unable to send LORA payload because not joined")
            return False

        # send
        responseData = self.sendPayload(payload)
        if responseData == False:
            self.noDownlinkCounter = self.noDownlinkCounter + 1
            return False

        # handle response
        if responseData != None and len(responseData) > 0:
            try:
                return True
            except Exception as e:
                self.log("ERROR: Unable to handle LORA payload: ", e.args[0],
                         e)
                self.noDownlinkCounter = self.noDownlinkCounter + 1
        else:
            self.noDownlinkCounter = self.noDownlinkCounter + 1
        # the message has been sent
        return True

    def sendTimeRequest(self, clockSyncEvent, clockSyncRequests):
        clockSyncEvent['Command'] = eventlog.CMD_TIME_REQUEST2
        payload = self.makePayload(clockSyncEvent)
        try:
            with self.sendLock:
                # send lora uplink
                responseData = self.sendPayload(payload, False)
                if responseData == False:
                    return None

        except Exception as e:
            self.log("ERROR", "Unable to sync clock via LORA:", e.args[0], e)
        return None

    # send the specified payload
    def sendPayload(self, data, updateTime=True):
        try:
            with self.socketLock:
                self.log("> sending", len(data), "bytes:",
                         binascii.hexlify(data))
                responseData = None
                # create a LoRa socket
                s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
                s.setblocking(False)
                try:
                    """free_memory = gc.mem_free()
                    allocated_memory = gc.mem_alloc()
                    print("Free Memory: " + str(free_memory) + " -- Allocated Memory : " + str(allocated_memory))"""
                    s.send(data)
                    time.sleep(5)
                    responseData = s.recv(64)
                except Exception as e:
                    self.log("ERROR", "LORA Socket Exception", e)
                s.close()
                if responseData != None:
                    responseLen = len(responseData)
                    if responseLen > 0:
                        self.log("< received", responseLen, "bytes:",
                                 binascii.hexlify(responseData))
                    else:
                        self.log("< no downlink")
                # log
                if updateTime == True:
                    self.lastUplinkTime = time.time()
                self.log(self.stats())
                time.sleep_ms(10)
                # save frame counters
                self.lora.nvram_save()
                time.sleep_ms(5)
                return responseData
        except Exception as e:
            self.log("ERROR", "Unable to send payload", e.args[0], e)
        return False
class LoraCoverage:

    def __init__(self, host, port, ssid, wpa, log_path):
        self.host = host
        self.port = port
        self.ssid = ssid
        self.wpa = wpa
        self.path = log_path

        self.gps_buffer = None
        self.log_time = None
        self.log_file = None
        self.rxnb = 0

        self.loramac = binascii.hexlify(network.LoRa().mac())

        # to be refactored
        self.end = False

        self.lock = _thread.allocate_lock()

        # Create proper directory for log file
        self._init_log()

        # Setup wypy/lopy as a station
        self.wlan = network.WLAN(mode=network.WLAN.STA)
        self.wlan.connect(self.ssid, auth=(network.WLAN.WPA2, self.wpa))

        while not self.wlan.isconnected():
            print('Connecting to Android WIFI HOTPOST...')
            time.sleep(1)

        print('Connected to Android WIFI HOTPOST')

        # Lora socket
        self.lora = None
        self.lora_sock = None

        # TCP socket
        self.tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Connect
        self.tcp_sock.connect((self.host, self.port))

    def _init_log(self):
        # create directory log if not exist
        try:
            os.mkdir(self.path)
        except OSError:
            pass

    # Check if string is empty
    def _isNotBlank (self, myString):
        return bool(myString and myString.strip())

    def _get_log_name(self):
        #print(self.log_time[:19])
        #print(type(ts))
        # ts = ts.replace('-', '')
        # ts = ts.replace('T', '')
        # ts = ts.replace(':', '')
        #ts = '1974'
        #base = self.path + '/acq/'
        #return base + ts + 'list.csv'
        return self.path + '/acq.list.csv'

    def _lora_cb(self, lora):
        events = lora.events()
        if (events & LoRa.RX_PACKET_EVENT) and (self.gps_buffer is not None):
            #if self.log_file is None:
            #    self.log_file = open(self._get_log_name(), 'w')

            data_rx = self.lora_sock.recv(256)
            stats = self.lora.stats()            # get lora stats (data is tuple)

            if self._isNotBlank(data_rx):
                with self.lock:
                    print(self.log_time)
                    print(self.gps_buffer)
                #print(self.gps_buffer['time'])
                # time_stamp = stats[0]          # get timestamp value
                # rssi = stats[1]
                # snr = stats[2]
                # sf = stats[3]
                #
                # msgData = ''
                # msgCrc = ''
                # if len(data_rx) >= 5:
                #     msg_data = data_rx[:-5]                  # remove the last 5 char data crc
                #     msg_crc = data_rx[-4:]                   # get the last 4 char
                #
                # # Calculate CRC
                # crc8 = utils.crc(msg_data)
                #
                # # Check CRC
                # crc_ok = True if crc8 == msg_crc.decode('utf-8') else False
                # # crc_ok = False
                # # if crc8 == msg_crc.decode('utf-8'):
                # #     crc_ok = True
                #
                # # fields_lorastats(LoraStats)
                #
                # # form csv row
                # msg = str(self.rxnb)
                # msg = msg + ',' + self.loramac.decode('utf8')
                # msg = msg + ',' + self.gps_buffer['time']
                # msg = msg + ',' + str(self.gps_buffer.lat)
                # msg = msg + ',' + str(self.gps_buffer.lon)
                # msg = msg + ',' + str(self.gps_buffer.alt)
                #
                # msg = msg + ',' + msg_data.decode('utf-8')
                # msg = msg + ',' + msg_crc.decode('utf-8')
                # msg = msg + ',' + str(crc8)
                # msg = msg + ',' + str(crc_ok)
                # msg = msg + ',' + str(time_stamp)
                # msg = msg + ',' + str(rssi)
                # msg = msg + ',' + str(snr)
                # msg = msg + ',' + str(sf)
                #
                # # # calc crc8 row
                # #crc8row = calc(msg.encode('utf-8'))
                # crc8row = utils.crc(msg.encode('utf-8'))
                #
                # # # add crc8row as last item
                # msg = msg + ',' + str(crc8row)
                #
                # # write csv and terminal
                # self.log_file.write(msg)
                # self.log_file.write('\n')
                # self.log_file.flush()
                #
                # print(msg)              # show in repl
                # self.rxnb += 1

    def start(self):
        # Initialize a LoRa sockect
        self.lora = LoRa(mode=LoRa.LORA)
        self.lora_sock = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
        self.lora_sock.setblocking(False)

        #self.tcp_alarm = Timer.Alarm(handler=lambda u: self._tcp_gps(), s=1, periodic=True)
        self.lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=self._lora_cb)

        # Start the TCP thread receiving GPS coordinates
        _thread.start_new_thread(self._tcp_thread, ())


    def stop(self):
        self.wlan.mode(network.WLAN.AP)
        self.tcp_sock.close()

        self.lora.callback(trigger=LoRa.RX_PACKET_EVENT, handler=None)
        self.lora_sock.close()
        #self.log_file.close()

        # Set end flag to terminate TCP thread
        self.end = True

    def _tcp_thread(self):
        while True:
            if self.end:
                _thread.exit()

            try:
                # Send request
                self.tcp_sock.send(b'?SHGPS.LOCATION;\r\n')
                # Get response
                rx_data = self.tcp_sock.recv(4096)

                # Release the lock in case of previous TCP error
                #self.lock.release()

                # Save last gps received to buffer
                with self.lock:
                    self.gps_buffer = json.loads(rx_data.decode('ascii'))
                    if not self.log_time:
                       self.log_time = self.gps_buffer['time']

            except socket.timeout:
                self.lock.locked()
            except OSError as e:
                if e.errno == errno.EAGAIN:
                    pass
                else:
                    self.lock.locked()
                    print("Error: Android 'ShareGPS' connection status should be 'Listening'")
                    print('Change mode and soft reboot Pycom device')
            except Exception:
                self.lock.locked()
                print("TCP recv Exception")
            time.sleep(0.5)
Example #29
0
class LoRaMQ:
    def __init__(self, config):
        self.lora = LoRa(mode=LoRa.LORAWAN)
        # set the 3 default channels to the same frequency (must be before sending the OTAA join request)
        self.lora.add_channel(0,
                              frequency=config.LORA_FREQUENCY,
                              dr_min=0,
                              dr_max=5)
        self.lora.add_channel(1,
                              frequency=config.LORA_FREQUENCY,
                              dr_min=0,
                              dr_max=5)
        self.lora.add_channel(2,
                              frequency=config.LORA_FREQUENCY,
                              dr_min=0,
                              dr_max=5)
        # join a network using OTAA
        self.lora.join(activation=LoRa.OTAA,
                       auth=(binascii.unhexlify(config.DEV_EUI),
                             binascii.unhexlify(config.APP_EUI),
                             binascii.unhexlify(config.APP_KEY)),
                       timeout=0,
                       dr=config.LORA_NODE_DR)
        # wait until the module has joined the network
        while not self.lora.has_joined():
            time.sleep(2.5)
            print('Not joined yet...')
        # remove all the non-default channels
        for i in range(3, 16):
            self.lora.remove_channel(i)
        # create a LoRa socket
        self.s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
        # set the LoRaWAN data rate
        self.s.setsockopt(socket.SOL_LORA, socket.SO_DR, config.LORA_NODE_DR)
        # make the socket non-blocking
        self.s.setblocking(False)
        time.sleep(5.0)
        self.send_mutex = _thread.allocate_lock()
        self.send_queue = []
        self.recv_mutex = _thread.allocate_lock()
        self.recv_queue = []
        self.callback = None
        self.lora.callback(LoRa.RX_PACKET_EVENT, self._recv)

    def _recv(self, data):
        rx, port = s.recvfrom(256)
        self.recv_mutex.acquire()
        self.recv_queue.append((port, rx))
        self.recv_mutex.release()
        if self.callback:
            _thread.start_new_thread(self.callback, tuple())

    def send(self, data):
        self.send_mutex.acquire()
        self.send_queue.append(data)
        self.send_mutex.release()

    def receive(self):
        self.recv_mutex.acquire()
        if not len(self.recv_queue):
            return False
        data = self.recv_queue.pop(0)
        self.recv_mutex.release()
        return data

    def rq_length(self):
        self.recv_mutex.acquire()
        size = len(self.recv_queue)
        self.recv_mutex.release()
        return size

    def attach_callback(self, callback):
        self.callback = callback

    def _loop(self):
        while True:
            #acquire a lock on the send message queue
            self.send_mutex.acquire()
            #get the message from the front of the queue
            if len(self.send_queue):
                data = self.send_queue.pop(0)
            else:
                data = None
            self.send_mutex.release()
            if data:
                #acquire a lock on the channel
                self.recv_mutex.acquire()
                #send the data
                self.s.send(data)
                #release the channel lock
                self.recv_mutex.release()
                #LoRa protocol only allows sending and recieving every 5 seconds
                #so to avoid protocol errors from the transciever sleep the thread
                #for 5 seconds
            time.sleep(5)

    def start(self):
        _thread.start_new_thread(self._loop, tuple())