Skip to content

rx radio listening interferes with gps updates #60

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
dewalex opened this issue Feb 13, 2021 · 23 comments
Closed

rx radio listening interferes with gps updates #60

dewalex opened this issue Feb 13, 2021 · 23 comments
Assignees

Comments

@dewalex
Copy link

dewalex commented Feb 13, 2021

I am very new to circuitpython. What I am trying to make is basically a much simpler version of the GlitterPos example: a lora gps using the Feather M0 lora chip + GPS Featherwing to make a dog tracker that sends out my dog's location every 4 seconds. I would like to add a feature where I can send the dog's tracker a message and have the update interval change from 4 seconds to 5 minutes.

The trouble that I am running into is that when I have rx listening (even at a timeout of 0.2), the gps data is ruined and it sends 0's in the dates, satellites, etc, and sends the same timestamp for about 15 to 20 seconds before grabbing another.

Can anyone tell me why this interference is happening?

Ideally I would like for the radio to be able to receive data while sending other data out, but I don't know how to make those happen concurrently and don't think Ive read that there is a possibility of parallel processes with this hardware. Either way, I didn't think that a short listening timeout would do this to the gps data though.

gpos.py:

"""Adapting from adafruit glitter positioning system example"""
import time
import board
import busio
import digitalio
from analogio import AnalogIn
import math

import adafruit_gps
import adafruit_rfm9x

import gc

RADIO_FREQ_MHZ = 915.0
CS = digitalio.DigitalInOut(board.RFM9X_CS)
RESET = digitalio.DigitalInOut(board.RFM9X_RST)

# Define the onboard LED
LED = digitalio.DigitalInOut(board.D13)
LED.direction = digitalio.Direction.OUTPUT

# ##### SETUP BATTERY LEVEL ## - Call it later
vbat_voltage = AnalogIn(board.VOLTAGE_MONITOR)
def get_voltage(pin):
    return (pin.value * 3.3) / 65536 * 2

class GPOS:

    def __init__(self):
        """configure sensors, radio, blinkenlights"""
        self.last_send = time.monotonic()
        
        # self.coords = (0, 0)
        self.init_radio()
        self.init_gps()

    def init_radio(self):
        """Set up RFM95."""
        spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
        self.rfm9x = adafruit_rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)
        self.rfm9x.tx_power = 23  # Default is 13 dB; the RFM95 goes up to 23 dB
        self.radio_tx('Radio initialized')
        time.sleep(1)

    def init_gps(self):
        """Set up GPS module."""
        uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=3)
        gps = adafruit_gps.GPS(uart)
        time.sleep(1)

        # https://cdn-shop.adafruit.com/datasheets/PMTK_A11.pdf
        # Turn on the basic GGA and RMC info (what you typically want), then
        # set update to once a second:
        gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0")
        gps.send_command(b"PMTK220,1000")
        time.sleep(1)

        self.gps = gps


    # ############################
    # ############################
    def advance_frame(self):
        """
        Check the radio for new packets, poll GPS and compass data, send a
        radio packet if coordinates have changed (or if it's been a while), and
        update NeoPixel display.  Called in an infinite loop by code.py.
        To inspect the state of the system, initialize a new GlitterPOS object
        from the CircuitPython REPL, and call gp.advance_frame() manually.  You
        can then access the instance variables defined in __init__() and
        init_()* methods.
        """
        # print("hey")  # see how fast looping is happening
        self.gps.update()
        current = time.monotonic()
        self.radio_rx(timeout=0.2)
        

        if not self.gps.has_fix:
            # Try again if we don't have a fix yet.
            LED.value = True
            time.sleep(0.05)
            LED.value = False
            return

        # We want to send coordinates out roughly
        # every 4 seconds:
        if (current - self.last_send < 4):
            return

        
        # gps_coords = (self.gps.latitude, self.gps.longitude)
        # if gps_coords == self.coords:
        #    return

        # self.coords = (self.gps.latitude, self.gps.longitude)

        
        # date, time, latitude, longitude, altitude, speed, angle, satellites, fixquality, hdop, geoid, batterylevel
        # put a comma after every number
        
        if self.gps.altitude_m is not None:
            altitud = "{:.0f}".format(self.gps.altitude_m)
        else:
            altitud = 0
        if self.gps.speed_knots is not None:
            speed = "{:.1f}".format(self.gps.speed_knots)
        else:
            speed = 0
        if self.gps.track_angle_deg is not None:
            angle = "{:.0f}".format(self.gps.track_angle_deg)
        else:
            angle = 0
        if self.gps.satellites is not None:
            satellites = self.gps.satellites
        else:
            satellites = 0
        """if self.gps.horizontal_dilution is not None:
            hdop = self.gps.horizontal_dilution
        else:
            hdop = 0
        if self.gps.height_geoid is not None:
            geoid = self.gps.height_geoid
        else:
            geoid = 0"""

        # ### Get battery level
        battery_voltage = get_voltage(vbat_voltage)
        battery_percentage = math.floor(
            (battery_voltage - 3.2) / 1 * 100
        )  

        
        # ### Packet and print the whole string comma delimited
        send_packet = "{}/{}/{}T{:02}:{:02}:{:02},{},{},{},{},{},{},{},{}".format(
            self.gps.timestamp_utc.tm_year,
            self.gps.timestamp_utc.tm_mon,  # Grab parts of the time from the struct_time object that holds
            self.gps.timestamp_utc.tm_mday,  # the fix time.  Note you might
            self.gps.timestamp_utc.tm_hour,  # not get all data like year, day,
            self.gps.timestamp_utc.tm_min,  # month!
            self.gps.timestamp_utc.tm_sec,
            "{0:.6f}".format(self.gps.latitude),
            "{0:.6f}".format(self.gps.longitude),
            altitud,
            speed,
            angle,
            satellites,
            self.gps.fix_quality, 
            # hdop,
            # geoid,
            "{:.0f}".format(battery_percentage),
        )


        # print('   quality: {}'.format(self.gps.fix_quality))
        print('   ' + str(gc.mem_free()) + " bytes free")

        # Send a location packet:
        self.radio_tx(send_packet)
        


    def radio_tx(self, msg):
        """send a packet over radio (I took away id prefix)"""
        packet = msg
        print('sending: ' + packet)

        # Blocking, max of 252 bytes:
        LED.value = True
        self.rfm9x.send(packet)
        LED.value = False
        self.last_send = time.monotonic()


    def radio_rx(self, timeout):
        """check radio for new packets, handle incoming data"""

        packet = self.rfm9x.receive(timeout=timeout)

        # If no packet was received during the timeout then None is returned:
        if packet is None:
            return

        packet = bytes(packet)
        
        # print('   received signal strength: {0} dB'.format(self.rfm9x.rssi))
        # print('   received (raw bytes): {0}'.format(packet))

        packet_text = str(packet, 'ascii')
        print('Received: {0}'.format(packet_text))
        
        # blink a lot to show receipt
        LED.value = True
        time.sleep(0.02)
        LED.value = False
        LED.value = True
        time.sleep(0.02)
        LED.value = False
        LED.value = True
        time.sleep(0.02)
        LED.value = False
        LED.value = True
        time.sleep(0.02)
        LED.value = False
        LED.value = True
        time.sleep(0.02)
        LED.value = False

code.py:

from gpos import GPOS

gp = GPOS()
while True:
    gp.advance_frame()
@jerryneedell
Copy link
Contributor

Sounds like a great project! this brings back memories of trying to use a GPS with the RFM9x and I recall having similar issues. In my case, i just stopped trying to receive and found it worked fine as long as I just sent packets. If I recall correctly, the issue was that the GPS expected gps.update to be called more frequently than was occurring.

I will try to reproduce this and see If I can get anywhere with it. It's been a long time since I looked at it.

Have you tried reducing the timeout even further -- say .1 seconds?
Also, what version of CircuitPython are you executing (it is displayed at the REPL start or in the contents of the boot_out.txt file on your board.
Also please confirm that out are using a FEATHER_RFM95_m0 board https://www.adafruit.com/product/3178 or if not, -- what are you using?

@jerryneedell jerryneedell self-assigned this Feb 14, 2021
@jerryneedell
Copy link
Contributor

jerryneedell commented Feb 14, 2021

I ran your code on my feather_m0_rfm95 and it seems to work OK
edited to add: It does take some time to get initialized and it does send some repeats.
Is this different from what you see?


Adafruit CircuitPython 6.2.0-beta.2-14-g766e79ad3 on 2021-02-14; Adafruit Feather M0 RFM9x with samd21g18
>>>
>>> import gpos_run
sending: Radio initialized
   4832 bytes free
sending: 0/0/0T13:56:51,43.143129,-70.922241,183,0,0,3,1,101
   3040 bytes free
sending: 0/0/0T13:56:51,43.143129,-70.922241,183,0,0,3,1,101
   6352 bytes free
sending: 0/0/0T13:56:57,43.143129,-70.922213,183,0,0,3,1,101
   4768 bytes free
sending: 0/0/0T13:56:57,43.143129,-70.922213,183,0,0,3,1,100
   2576 bytes free
sending: 0/0/0T13:56:57,43.143129,-70.922213,183,0,0,3,1,100
   6464 bytes free
sending: 2021/2/14T13:57:11,43.142991,-70.922122,183,0.7,134,3,1,101
   1056 bytes free
sending: 2021/2/14T13:57:18,43.142929,-70.922089,183,0.7,134,3,1,100
   4768 bytes free
sending: 2021/2/14T13:57:22,43.142900,-70.922060,183,0.7,134,3,1,101
   2544 bytes free
sending: 2021/2/14T13:57:22,43.142900,-70.922060,183,0.7,134,3,1,101

edited to add -- here is what I am receiving on another board -- with some diagnostics

Received (raw header): ['0xff', '0xff', '0x0', '0x0']
Received (raw payload): bytearray(b'0/0/0T13:56:51,43.143129,-70.922241,183,0,0,3,1,101')
RSSI: -48
RSSIx: -48
SNR: 5.25
SNRx: 5.25
Received (raw header): ['0xff', '0xff', '0x0', '0x0']
Received (raw payload): bytearray(b'0/0/0T13:56:57,43.143129,-70.922213,183,0,0,3,1,100')
RSSI: -48
RSSIx: -48
SNR: 5.5
SNRx: 5.5
Received (raw header): ['0xff', '0xff', '0x0', '0x0']
Received (raw payload): bytearray(b'0/0/0T13:56:57,43.143129,-70.922213,183,0,0,3,1,100')
RSSI: -47
RSSIx: -47
SNR: 5.5
SNRx: 5.5
Received (raw header): ['0xff', '0xff', '0x0', '0x0']
Received (raw payload): bytearray(b'2021/2/14T13:57:11,43.142991,-70.922122,183,0.7,134,3,1,101')
RSSI: -48
RSSIx: -48
SNR: 5.5
SNRx: 5.5
Received (raw header): ['0xff', '0xff', '0x0', '0x0']
Received (raw payload): bytearray(b'2021/2/14T13:57:22,43.142900,-70.922060,183,0.7,134,3,1,101')

@dewalex
Copy link
Author

dewalex commented Feb 14, 2021

Thanks for replying so quickly! Yes, that is the board that I using and I am running CP 6.1.0. I did try going really low with the rx receive but I was still having the same issue.

Last night I tried sprinkling the gps.update() call through the script and then I did see just about what you are showing (i.e. 0/0/0 date for a while and then after that a string that looks ok, but still repeats every once in a while). I think I can live with some double reports, but what I was getting before was 4 or 5 in a row that were the same.

I tried looking into why the gps would take some time to initialize and give 0's in the dates, angles, etc, but I couldn't really find the documentation. What causes the gps.update() call to fail to parse if it isn't repeating a ton, and what is the deal with the initialization? (I assumed that if there is a fix, then the date should come through alright, but I'm surely wrong about that)

I thought about just having the dog's unit transmit and not receive, but I would really like to be able to put it into low update mode (if he is close to running out of battery and not heading back to me, I don't want it updating at a high rate and run out of battery).

I ended up starting a new script with most of the same components, and for whatever reason it is performing better with the updates. It still seems to delay the loop longer than the timeout I put on the rx...but as long as I'm receiving my strings in time, that's ok. I put in there that if it receives "q", change mode to 300 second updates instead of 5 seconds, and if it receives "s", then send a small status packet. This script seems to be working ok, except that it runs for about 15 minutes and then there is an error with receiving corrupted data (I may have had crc off then...maybe that will fix it?). The error I am getting is TypeError: object with buffer protocol required.

Is there any way to ignore bad data to keep the board from crashing if it receives a corrupted packet?

Here is the updated script (just all in code.py now, not using a class):

# ## GPS LoRa code.py

import time
import board
import busio
import digitalio
from analogio import AnalogIn
import math

import adafruit_gps
import adafruit_rfm9x

# Define the onboard LED
LED = digitalio.DigitalInOut(board.D13)
LED.direction = digitalio.Direction.OUTPUT

# Create a serial connection for the GPS connection
uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10)

# #### SET UP RADIO ####
cs = digitalio.DigitalInOut(board.RFM9X_CS)
reset = digitalio.DigitalInOut(board.RFM9X_RST)
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)

rfm9x = adafruit_rfm9x.RFM9x(spi, cs, reset, 915.0)  # radio is set to 915MHz
rfm9x.tx_power = 23  # Default is 13 dB; the RFM95 goes up to 23 dB

# set node addresses  #maybe that will keep from receiving bad packet from somewhere else
rfm9x.node = 1
rfm9x.destination = 2

# Apply new modem config settings to the radio to improve its effective range
"""Custom configs:
    { 0x72,   0x74,    0x00}, // Bw125Cr45Sf128 (the chip default)
    { 0x92,   0x74,    0x00}, // Bw500Cr45Sf128
    { 0x48,   0x94,    0x00}, // Bw31_25Cr48Sf512
    { 0x78,   0xc4,    0x00}, // Bw125Cr48Sf4096
 """

rfm9x.signal_bandwidth = 150000
rfm9x.coding_rate = 6
rfm9x.spreading_factor = 10
rfm9x.enable_crc = True

print("Radio Initialized")
time.sleep(0.5)


# ###Create a GPS module instance. #######
gps = adafruit_gps.GPS(uart, debug=False)  # Use UART/pyserial

# Turn on the basic GGA and RMC info (what you typically want)
gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0")


# Set update rate to once a second (1hz)
gps.send_command(b"PMTK220,1000")


# ##### SETUP BATTERY LEVEL ######### -- Call it later in the loop
vbat_voltage = AnalogIn(board.VOLTAGE_MONITOR)
def get_voltage(pin):
    return (pin.value * 3.3) / 65536 * 2

# the quick or slow mode will be set by incoming transmission and will change this number
sendinterval = 5


# Main loop runs forever printing the location, etc. every second.
last_send = time.monotonic()
while True:

    gps.update()
    # print("hey")  # see how fast it is looping

    # ### Receive #####
    packet = rfm9x.receive(timeout=0.5)  # Wait for a packet to be received (up to 0.5 seconds)
    gps.update()
    if packet is not None:
        packet_text = str(packet, 'ascii')

        if packet_text == str(bytes("q", "utf-8"),'ascii'):
            if sendinterval == 5:
                sendinterval = 300
            else:
                sendinterval = 5
            print(sendinterval)
            LED.value = True
            rfm9x.send(bytes(str(sendinterval), "utf-8"))
            LED.value = False


        if packet_text == str(bytes("s", "utf-8"),'ascii'):

            status = "{},{:02}:{:02}:{:02},{},{},{}".format(
            "s",
            gps.timestamp_utc.tm_hour,  # not get all data like year, day,
            gps.timestamp_utc.tm_min,  # month!
            gps.timestamp_utc.tm_sec,
            "{0:.6f}".format(gps.latitude),
            "{0:.6f}".format(gps.longitude),
            sendinterval
            )
            print(status)
            LED.value = True
            rfm9x.send(bytes(status, "utf-8"))
            LED.value = False

        time.sleep(0.01)



    # Every second print out current location details if there's a fix.
    gps.update()
    current = time.monotonic()
    if current - last_send >= sendinterval:

        if not gps.has_fix:
            # Try again if we don't have a fix yet.
            print("Waiting for fix...")
            continue
        

        # ###############################################
        # date, time, latitude, longitude, altitude, speed, angle, satellites, fixquality, hdop, geoid, sendinterval,batterylevel
        # put a comma after every number

        if gps.altitude_m is not None:
            altitud = "{:.0f}".format(gps.altitude_m)
        else:
            altitud = 0
        if gps.speed_knots is not None:
            speed = "{:.1f}".format(gps.speed_knots)
        else:
            speed = 0
        if gps.track_angle_deg is not None:
            angle = "{:.0f}".format(gps.track_angle_deg)
        else:
            angle = 0
        if gps.satellites is not None:
            satellites = gps.satellites
        else:
            satellites = 0
        if gps.horizontal_dilution is not None:
            hdop = gps.horizontal_dilution
        else:
            hdop = 0
        if gps.height_geoid is not None:
            geoid = gps.height_geoid
        else:
            geoid = 0

        # ### Get battery level
        battery_voltage = get_voltage(vbat_voltage)
        battery_percentage = math.floor(
            (battery_voltage - 3.2) / 0.9 * 100
        )  # divide by 0.9 because 4.1 is max and 3.2 is min battery, the distance between them is 0.9

        # ### Packet and print the whole string comma delimited
        packetforsend = "{}/{}/{}T{:02}:{:02}:{:02},{},{},{},{},{},{},{},{},{}".format(
            gps.timestamp_utc.tm_year,
            gps.timestamp_utc.tm_mon,  # Grab parts of the time from the struct_time object that holds
            gps.timestamp_utc.tm_mday,  # the fix time.  Note you might
            gps.timestamp_utc.tm_hour,  # not get all data like year, day,
            gps.timestamp_utc.tm_min,  # month!
            gps.timestamp_utc.tm_sec,
            "{0:.6f}".format(gps.latitude),
            "{0:.6f}".format(gps.longitude),
            altitud,
            speed,
            angle,
            satellites,
            gps.fix_quality, # take away these three because receiver can't process it
            # hdop,
            # geoid,
            sendinterval,
            "{:.0f}".format(battery_percentage),
        )


        print(packetforsend)

        # ## SEND
        LED.value = True
        rfm9x.send(bytes(packetforsend, "utf-8"))
        last_send = time.monotonic()
        LED.value = False

@jerryneedell
Copy link
Contributor

jerryneedell commented Feb 14, 2021

Occasional noise packets are a fact of life for me

To trap a particular exception you could do something like this:

try:
    packet = rfm9x.receive(timeout=0.5)  # Wait for a packet to be received (up to 0.5 seconds)
except TypError:
    print("bad packet - ignore")
    continue

That should go back to the top of the While True loop and listen for another packet. Take a look at other uses for the try/except statements in Python. That can be very helpful in avoiding crashes.

I'll try to run your updated code see how it works.
Edited to add:
Upa and running -- with the try/except -- I need to modify to receiver code to use your settings -- I went back to defaults for now. It'll be tomorrow before I can do anything more with it.

Sounds like you are making great progress.
I have not spent a lot of time looking into the GPS behavior. This has been a good learning experience for me. I'm not convinced there is really an issue with the RFM9X code but please provide any examples if you think its is not behaving correctly.
I am really pleased to see that this code can run with the GPS driver on this board.
I don't think that would have been possible before the RFM9X code was "frozen" into the build for this board.

@dewalex
Copy link
Author

dewalex commented Feb 14, 2021

Excellent, thanks for that, I'll write it in!

Yes, all of the examples and documentation in the Learn section of the website have helped me move forward quickly. I just ordered the hardware 2 weeks ago and started out with Arduino. I was doing ok trying to adapt other people's code, but customization was getting cumbersome for me since there was so much I didn't understand, so I started looking at CircuitPython last week. I've only ever coded with R, and Python is so much closer to that language. If there's something that if I don't understand in Python, I can usually find the answer by looking it up and understand it (NOT the case with C++).

I'll carry on testing and let you know if I see anymore unexpected behavior. Appreciate your help.

@jerryneedell
Copy link
Contributor

jerryneedell commented Feb 15, 2021

@dewalex I have been running your code and it is working quite well with occasional dropped packets when I use the default radio configuration. When I use the parameters you specified, I seem to be dropping more packets. My radios are quite close together at this time. < 1 meter apart. I was curious how it is working for you. I have not done a lot of experimentation with the radio parameter settings.

Edited to add:
I also tried using "Reliable Datagram Mode" by using
rfm9x.send_with_ack() for transmit
and adding
with_ack=True as a parameter to rfm9x.receive()

This enables an automatic "ack packet" handshake and automatic retries.
This does avoid some the dropped packets.
For your reference,
here is the code I am running on on the M0 -- similar changes have to be made on the receiving program

# ## GPS LoRa code.py

import time
import board
import busio
import digitalio
from analogio import AnalogIn
import math

import adafruit_gps
import adafruit_rfm9x

# Define the onboard LED
LED = digitalio.DigitalInOut(board.D13)
LED.direction = digitalio.Direction.OUTPUT

# Create a serial connection for the GPS connection
uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10)

# #### SET UP RADIO ####
cs = digitalio.DigitalInOut(board.RFM9X_CS)
reset = digitalio.DigitalInOut(board.RFM9X_RST)
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)

rfm9x = adafruit_rfm9x.RFM9x(spi, cs, reset, 915.0)  # radio is set to 915MHz
rfm9x.tx_power = 23  # Default is 13 dB; the RFM95 goes up to 23 dB

# set node addresses  #maybe that will keep from receiving bad packet from somewhere else
rfm9x.node = 1
rfm9x.destination = 2

# Apply new modem config settings to the radio to improve its effective range
"""Custom configs:
    { 0x72,   0x74,    0x00}, // Bw125Cr45Sf128 (the chip default)
    { 0x92,   0x74,    0x00}, // Bw500Cr45Sf128
    { 0x48,   0x94,    0x00}, // Bw31_25Cr48Sf512
    { 0x78,   0xc4,    0x00}, // Bw125Cr48Sf4096
 """

#rfm9x.signal_bandwidth = 150000
#rfm9x.coding_rate = 6
#rfm9x.spreading_factor = 10
rfm9x.enable_crc = True

print("Radio Initialized")
time.sleep(0.5)


# ###Create a GPS module instance. #######
gps = adafruit_gps.GPS(uart, debug=False)  # Use UART/pyserial

# Turn on the basic GGA and RMC info (what you typically want)
gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0")


# Set update rate to once a second (1hz)
gps.send_command(b"PMTK220,1000")


# ##### SETUP BATTERY LEVEL ######### -- Call it later in the loop
vbat_voltage = AnalogIn(board.VOLTAGE_MONITOR)
def get_voltage(pin):
    return (pin.value * 3.3) / 65536 * 2

# the quick or slow mode will be set by incoming transmission and will change this number
sendinterval = 5


# Main loop runs forever printing the location, etc. every second.
last_send = time.monotonic()
while True:

    gps.update()
    # print("hey")  # see how fast it is looping

    # ### Receive #####
    try:
        packet = rfm9x.receive(with_ack=True,timeout=0.5)  # Wait for a packet to be received (up to 0.5 seconds)
    except TypeError:
        print("bad packet - ignore")
        continue
    gps.update()
    if packet is not None:
        packet_text = str(packet, 'ascii')
        #time.sleep(.25)
        if packet_text == str(bytes("q", "utf-8"),'ascii'):
            if sendinterval == 5:
                sendinterval = 300
            else:
                sendinterval = 5
            print(sendinterval)
            LED.value = True
            rfm9x.send_with_ack(bytes(str(sendinterval), "utf-8"))
            LED.value = False


        if packet_text == str(bytes("s", "utf-8"),'ascii'):

            status = "{},{:02}:{:02}:{:02},{},{},{}".format(
            "s",
            gps.timestamp_utc.tm_hour,  # not get all data like year, day,
            gps.timestamp_utc.tm_min,  # month!
            gps.timestamp_utc.tm_sec,
            "{0:.6f}".format(gps.latitude),
            "{0:.6f}".format(gps.longitude),
            sendinterval
            )
            print(status)
            LED.value = True
            rfm9x.send_with_ack(bytes(status, "utf-8"))
            LED.value = False

        time.sleep(0.01)



    # Every second print out current location details if there's a fix.
    gps.update()
    current = time.monotonic()
    if current - last_send >= sendinterval:

        if not gps.has_fix:
            # Try again if we don't have a fix yet.
            print("Waiting for fix...")
            continue
        

        # ###############################################
        # date, time, latitude, longitude, altitude, speed, angle, satellites, fixquality, hdop, geoid, sendinterval,batterylevel
        # put a comma after every number

        if gps.altitude_m is not None:
            altitud = "{:.0f}".format(gps.altitude_m)
        else:
            altitud = 0
        if gps.speed_knots is not None:
            speed = "{:.1f}".format(gps.speed_knots)
        else:
            speed = 0
        if gps.track_angle_deg is not None:
            angle = "{:.0f}".format(gps.track_angle_deg)
        else:
            angle = 0
        if gps.satellites is not None:
            satellites = gps.satellites
        else:
            satellites = 0
        if gps.horizontal_dilution is not None:
            hdop = gps.horizontal_dilution
        else:
            hdop = 0
        if gps.height_geoid is not None:
            geoid = gps.height_geoid
        else:
            geoid = 0

        # ### Get battery level
        battery_voltage = get_voltage(vbat_voltage)
        battery_percentage = math.floor(
            (battery_voltage - 3.2) / 0.9 * 100
        )  # divide by 0.9 because 4.1 is max and 3.2 is min battery, the distance between them is 0.9

        # ### Packet and print the whole string comma delimited
        packetforsend = "{}/{}/{}T{:02}:{:02}:{:02},{},{},{},{},{},{},{},{},{}".format(
            gps.timestamp_utc.tm_year,
            gps.timestamp_utc.tm_mon,  # Grab parts of the time from the struct_time object that holds
            gps.timestamp_utc.tm_mday,  # the fix time.  Note you might
            gps.timestamp_utc.tm_hour,  # not get all data like year, day,
            gps.timestamp_utc.tm_min,  # month!
            gps.timestamp_utc.tm_sec,
            "{0:.6f}".format(gps.latitude),
            "{0:.6f}".format(gps.longitude),
            altitud,
            speed,
            angle,
            satellites,
            gps.fix_quality, # take away these three because receiver can't process it
            # hdop,
            # geoid,
            sendinterval,
            "{:.0f}".format(battery_percentage),
        )


        print(packetforsend)

        # ## SEND
        LED.value = True
        rfm9x.send_with_ack(bytes(packetforsend, "utf-8"))
        last_send = time.monotonic()
        LED.value = False

@dewalex
Copy link
Author

dewalex commented Feb 15, 2021

@jerryneedell Thanks for your reply. I increased the Spread Factor and reduced the Bandwidth to try to get more distance, and in testing before writing this code I have gotten best results from the max Spread Factor 12. The problem is that it takes a couple of seconds to send, which would really interfere with any incoming commands.

The settings that I have above increase the Spread Factor but don't block everything for too long, although, I could not have a timeout of 0.5 and still get good GPS data (they were repeating for 25 seconds at a time). When I lowered to 0.2 timeout, I was getting good reports every 5 seconds. This means that I have to send it commands ("s" for a status update or "q" for a change in report interval) 3 or 4 times before it actually receives them. That's not so much of a problem as long as it sends the status or tells my Rx that it got the interval command and is changing modes.

I'll add in the ACK and let you know if it makes the process take long and interferes with the gps.update() or not. ...still don't understand the mechanics of that call and why it needs to loop so many times to give a good reading.

@dewalex
Copy link
Author

dewalex commented Feb 15, 2021

One other issue I am having is that my Rx is the M0 Bluefruit Feather. I would like to find a way to stream incoming data to my computer or phone. I'm not sure if I am just out of space with the rfm9x and bluefruitspi libraries being stored on the chip (I have 4kb still left to work with), or if there is an incompatibility with the two libraries. Both libraries are from the 6.x bundle.

If it is an incompatibility, we can split this into a new issue thread.

Just adding the import like so

import board
import busio
import time
import digitalio
from digitalio import DigitalInOut, Direction, Pull
from adafruit_bluefruitspi import BluefruitSPI

import adafruit_rfm9x

gives this error:

code.py output:
Traceback (most recent call last):
  File "code.py", line 11, in <module>
  File "adafruit_rfm9x.py", line 112, in <module>
  File "adafruit_rfm9x.py", line 608, in RFM9x
MemoryError: memory allocation failed, allocating 376 bytes

The line 112 for me is just this:

    current = time.monotonic()
    if current - last_print >= 5.0:
        last_print = current

@jerryneedell
Copy link
Contributor

It is very unlikely you will be able to use the bluefruit library and the gps library on an RFM9x_M0. The is just not enough RAM. The line 112 reference is in the adafruit_rfm9x library code, not your code,py but that may not be very meaningful.
You can try swapping the order of imports to see if it helps, but I would not expect it to. The M0 is severely limited.

@jerryneedell
Copy link
Contributor

Oh wait ,I was confused, this is not the GPS unit, but it is a bluefruit Feather M0 -- there you have to install the rfm9x library and the bluefruit library - the RFM9x is not "frozen in " like it is for the RFM95_m0 board. Again, there just may not be enough RAM for both the rfm9x and bluefruitspi libraries.

@jerryneedell
Copy link
Contributor

ah - I do have an M0 Bluefruit -- It'll try it on mine ad see if I can get anywhere with it. Are you using the RFM9X featherwing ir a breakout with it?

@jerryneedell
Copy link
Contributor

jerryneedell commented Feb 15, 2021

What version of CircuitPython did you put on the Bluefruit M0 -- I am able to load all the libraries using the "Adalogger" .uf2 file.
The Adalogger exposes pin D8 which is needed to access the Bluefruit module.
I'll see if I can actually get it to receive...


Adafruit CircuitPython 6.2.0-beta.2-14-g766e79ad3 on 2021-02-15; Adafruit Feather M0 Adalogger with samd21g18
>>> import board
>>> import busio
>>> import time
>>> import digitalio
>>> from digitalio import DigitalInOut, Direction, Pull
>>> from adafruit_bluefruitspi import BluefruitSPI
>>> 
>>> import adafruit_rfm9x
>>> import gc
>>> gc.mem_free()
6160
>>> 

@dewalex
Copy link
Author

dewalex commented Feb 15, 2021

Yes, sorry, should have mentioned that it is the RFM9x featherwing. Dogs unit is: RFM9x M0 + GPS featherwing, my unit is: Bluefruit M0 + RFM9x featherwing. I put the adalogger uf2 on the bluefruit m0, using this example: https://learn.adafruit.com/munny-lamp/code-with-circuitpython.

Thanks for giving it a try, it would be really cool if I could pass the data over bluetooth so I can eventually be able to use the tracker with a phone to see the locations being sent.

Here is my Rx code, if you want to use that. On the bluetooth side, ive just gotten as far as importing the library and trying to read the spi.

import board
import busio
import time
import digitalio
from digitalio import DigitalInOut, Direction, Pull
from adafruit_bluefruitspi import BluefruitSPI

import adafruit_rfm9x


# Define radio parameters.
RADIO_FREQ_MHZ = 915.0  # Frequency of the radio in Mhz.


# Define pins connected to the chip, use these if wiring up the breakout according to the guide:
CS = digitalio.DigitalInOut(board.D10)  # D10 ("B" on the board)
RESET = digitalio.DigitalInOut(board.D11) # D11 ("A" on the board)

# Define the onboard LED
LED = digitalio.DigitalInOut(board.D13)
LED.direction = digitalio.Direction.OUTPUT

# using D5 and D9 (E and C on the board) to send the status and interval change commands on button click
switch = DigitalInOut(board.D5)  # set up the input to make the command send and the led flash
switch.direction = Direction.INPUT
switch.pull = Pull.UP

switch2 = DigitalInOut(board.D9)  # set up the input to make the command send and the led flash
switch2.direction = Direction.INPUT
switch2.pull = Pull.UP


# Initialize SPI bus.
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)

# Initialze RFM radio
rfm9x = adafruit_rfm9x.RFM9x(spi, CS, RESET, RADIO_FREQ_MHZ)

# Note that the radio is configured in LoRa mode so you can't control sync
# word, encryption, frequency deviation, or other settings!

# You can however adjust the transmit power (in dB).  The default is 13 dB but
# high power radios like the RFM95 can go up to 23 dB:
rfm9x.tx_power = 23

# set node addresses (so only receives data from this LoRa)
rfm9x.node = 2
rfm9x.destination = 1


# Apply new modem config settings to the radio to improve its effective range
"""Custom configs:
    { 0x72,   0x74,    0x00}, // Bw125Cr45Sf128 (the chip default)
    { 0x92,   0x74,    0x00}, // Bw500Cr45Sf128
    { 0x48,   0x94,    0x00}, // Bw31_25Cr48Sf512
    { 0x78,   0xc4,    0x00}, // Bw125Cr48Sf4096
"""
# best so far: Bw125Cr48Sf4096  # Maybe test Bw31_25Cr48Sf512 again...but it was worse
rfm9x.signal_bandwidth = 150000
rfm9x.coding_rate = 6
rfm9x.spreading_factor = 10
rfm9x.enable_crc = True

# Can only send a packet up to 252 bytes.Each send waits for the previous one to finish before continuing
# rfm9x.send(bytes("Hello world!\r\n", "utf-8"))
# print("Sent Hello World message!")

# ### Bluetooth init
# spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
bcs = DigitalInOut(board.D8)
birq = DigitalInOut(board.D7)
brst = DigitalInOut(board.D4)
# bluefruit = BluefruitSPI(spi, bcs, birq, brst, debug=False)


print("Waiting for packets...")

last_print = time.monotonic()

# initiate send mode object
sendmode = "q"
thing = 1
while True:

    # ## If button is pressed, change update mode
    if switch.value:
        LED.value = False
    else:
        LED.value = True
        rfm9x.send(bytes("q", "utf-8"))
        print("q")
        LED.value = False
        time.sleep(0.4)  # debounce delay

    # ## If button2 is pressed, change update mode
    if switch2.value:
        LED.value = False
    else:
        LED.value = True
        rfm9x.send(bytes("s", "utf-8"))
        print("s")
        LED.value = False

        statuspacket = rfm9x.receive(timeout=1)
        if statuspacket is not None:
            status = str(statuspacket, "ascii")
            print(status)
        time.sleep(0.4)  # debounce delay for the "button"
    # ###################################
    # print("hey", thing)  # see how fast everything is looping
    # thing = thing +1

    current = time.monotonic()
    if current - last_print >= 5.0:
        last_print = current

        try:
            packet = rfm9x.receive(timeout=1)  # process is looping faster if I use the counter above
        except TypError:
            print("bad packet - ignore")
            continue

        # If no packet was received during the timeout then None is returned.
        if packet is None:
            # Packet has not been received
            LED.value = False
            # print("Received nothing! Listening again...")
        else:
            # Received a packet!
            LED.value = True

            #  decode packet
            packet_text = str(packet, "ascii")
            print( "{},{}".format(packet_text, "RSSI:{}".format(rfm9x.last_rssi)) )

            LED.value = False

@dewalex
Copy link
Author

dewalex commented Feb 15, 2021

What's the concept behind "frozen in" libraries? Does that mean that for libraries that are frozen in, I don't need to add them to the lib folder on the chip?

@dewalex
Copy link
Author

dewalex commented Feb 15, 2021

@jerryneedell The version of CP on the Bluefruit M0 is also 6.1.

@jerryneedell
Copy link
Contributor

jerryneedell commented Feb 15, 2021

Yes -- If they are listed when you run help('modules') at the REPL, then you don't have to put them on the board. The RFM9x has adafruit_rfm9x and adafruit_busdevice built in.
However, the adalogger has neither so they are needed there.

I am using the 6.2 beta versions but there may not be a big difference -- you can get the latest from the downloads pages for the boards https://circuitpython.org/board/feather_m0_adalogger/ to see if they perform any better.

@jerryneedell
Copy link
Contributor

jerryneedell commented Feb 15, 2021

Ugh! this is messier than I thought. I'll spend some time seeing If I can get anywhere with the Bluefruit board. It may take some experimenting with some "custom" builds. I'm not overly optimistic that it will work but I'm happy to experiment with it over the next few days.

An nRF5280 board would probably be far more well suited since it has much more RAM and supports the bleio module.

@jerryneedell
Copy link
Contributor

I am able to run your code on my feather_m0_bluefruit with the latest build of CircuitPython for the Adalogger. I have not tried 6.1

I even went so afar as to uncomment the line

# bluefruit = BluefruitSPI(spi, bcs, birq, brst, debug=False)

and it still runs. Encouraging.
I have not yet tried sending using the bluefruit connection.

@dewalex
Copy link
Author

dewalex commented Feb 15, 2021

I just upgraded the M0 to the 6.2 beta and I am no longer getting the error.

@jerryneedell
Copy link
Contributor

I've been trying to test the Bluefruit connection as well. I have found that importing the rfm9x library before the Bluefruitspi library also helps avoid memory allocation errors.
So far having mixed results getting it working.

@dewalex
Copy link
Author

dewalex commented Feb 20, 2021

@jerryneedell
Looking through issues on the adafruit_gps thread (adafruit/Adafruit_CircuitPython_GPS#18), it seems that we can solve the issue with the receive timeout interfering with the gps.update() by putting it in a while True: . Now I'm able to listen for commands on 1 second timeouts and still get gps updates sent out every 4 to 7 seconds without any duplicates at all.

The tx code (note: erase the node addresses unless you have set them on the receiver unit):

# ## GPS LoRa

import time
import board
import busio
import digitalio
from analogio import AnalogIn
import math

import adafruit_gps
import adafruit_rfm9x

# Define the onboard LED
LED = digitalio.DigitalInOut(board.D13)
LED.direction = digitalio.Direction.OUTPUT

# Create a serial connection for the GPS connection using default speed and
# a slightly higher timeout (GPS modules typically update once a second).
# These are the defaults you should use for the GPS FeatherWing.
# For other boards set RX = GPS module TX, and TX = GPS module RX pins.
uart = busio.UART(board.TX, board.RX, baudrate=9600, timeout=10)


# #### SET UP RADIO ####
cs = digitalio.DigitalInOut(board.RFM9X_CS)
reset = digitalio.DigitalInOut(board.RFM9X_RST)
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)

rfm9x = adafruit_rfm9x.RFM9x(spi, cs, reset, 915.0)  # radio is set to 915MHz
rfm9x.tx_power = 23  # Default is 13 dB; the RFM95 goes up to 23 dB

# set node addresses
rfm9x.node = 1
rfm9x.destination = 2


# Apply new modem config settings to the radio to improve its effective range
"""Custom configs:
    { 0x72,   0x74,    0x00}, // Bw125Cr45Sf128 (the chip default)
    { 0x92,   0x74,    0x00}, // Bw500Cr45Sf128
    { 0x48,   0x94,    0x00}, // Bw31_25Cr48Sf512
    { 0x78,   0xc4,    0x00}, // Bw125Cr48Sf4096
 """

rfm9x.signal_bandwidth = 250000
rfm9x.coding_rate = 4
rfm9x.spreading_factor = 11
rfm9x.enable_crc = True


print("Radio Initialized")
time.sleep(0.5)


# ###Create a GPS module instance. #######
gps = adafruit_gps.GPS(uart, debug=False)  # Use UART/pyserial

# Turn on the basic GGA and RMC info (what you typically want)
gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0")

# Set update rate to once a second (1hz) which is what you typically want.
gps.send_command(b"PMTK220,1000")


# ##### SETUP BATTERY LEVEL ######### -- Call it later in the loop
vbat_voltage = AnalogIn(board.VOLTAGE_MONITOR)
def get_voltage(pin):
    return (pin.value * 3.3) / 65536 * 2

# the quick or slow mode will be set by incoming transmission and will change this number
sendinterval = 4  # change number in two places below to change update interval


# Main loop 
last_send = time.monotonic()
while True:
    try:

        #  put the update in a while True: so that it doesn't send duplicates when it fails to update on the loop
        while True:
            gps.update()
            if gps.update():
                break
        # print("hey")

        # ### Receive #####
        try:
            packet = rfm9x.receive(timeout=1)  # Wait for a packet to be received 
        except TypeError:
            print("bad packet - ignore")
            continue

        if packet is not None:
            packet_text = str(packet, 'ascii')
            # print(packet_text)

            # when receive command to change update interval, change it to the opposite of the one it is on
            if packet_text == str(bytes("q", "utf-8"),'ascii'):
                if sendinterval == 4:
                    sendinterval = 300
                    pack = "q300"
                else:
                    sendinterval = 4
                    pack = "q5"

                print(pack)
                LED.value = True
                rfm9x.send(bytes(pack, "utf-8"))
                LED.value = False

            # when receive command for status, send a status
            if packet_text == str(bytes("s", "utf-8"),'ascii'):

                status = "{},{}/{}/{}T{:02}:{:02}:{:02},{},{},{}".format(
                "s",
                gps.timestamp_utc.tm_year,gps.timestamp_utc.tm_mon, gps.timestamp_utc.tm_mday,
                gps.timestamp_utc.tm_hour,  # not get all data like year, day,
                gps.timestamp_utc.tm_min,  # month!
                gps.timestamp_utc.tm_sec,
                "{0:.6f}".format(gps.latitude),
                "{0:.6f}".format(gps.longitude),
                sendinterval
                )
                print(status)
                LED.value = True
                rfm9x.send(bytes(status, "utf-8"))
                LED.value = False

            time.sleep(0.01)



        current = time.monotonic()
        if current - last_send >= sendinterval:
        #    last_print = current #moved to after sent as "last_send"


            if not gps.has_fix:
                # Try again if we don't have a fix yet.
                print("Waiting for fix...")
                continue
            # We have a fix! (gps.has_fix is true)
            # Print out details about the fix like location, date, etc.

            # print("=" * 40)  # Print a separator line.
            # ###############################################
            # date, time, latitude, longitude, altitude, speed, angle, satellites, fixquality, hdop, geoid, sendinterval,batterylevel
            # put a comma after every number

            if gps.altitude_m is not None:
                altitud = "{:.0f}".format(gps.altitude_m)
            else:
                altitud = 0
            if gps.speed_knots is not None:
                speed = "{:.1f}".format(gps.speed_knots)
            else:
                speed = 0
            if gps.track_angle_deg is not None:
                angle = "{:.0f}".format(gps.track_angle_deg)
            else:
                angle = 0
            if gps.satellites is not None:
                satellites = gps.satellites
            else:
                satellites = 0
            if gps.horizontal_dilution is not None:
                hdop = gps.horizontal_dilution
            else:
                hdop = 0
            if gps.height_geoid is not None:
                geoid = gps.height_geoid
            else:
                geoid = 0

            # ### Get battery level
            battery_voltage = get_voltage(vbat_voltage)
            battery_percentage = math.floor(
            (battery_voltage - 3.2) / 0.9 * 100
            )  # divide by 0.9 because 4.1 is max and 3.2 is min battery, the distance between them is 0.9

            # ### Packet and print the whole string comma delimited
            packetforsend = "{}/{}/{}T{:02}:{:02}:{:02},{},{},{},{},{},{},{},{},{}".format(
                gps.timestamp_utc.tm_year,
                gps.timestamp_utc.tm_mon,  # Grab parts of the time from the struct_time object that holds
                gps.timestamp_utc.tm_mday,  # the fix time.  Note you might
                gps.timestamp_utc.tm_hour,  # not get all data like year, day,
                gps.timestamp_utc.tm_min,  # month!
                gps.timestamp_utc.tm_sec,
                "{0:.6f}".format(gps.latitude),
                "{0:.6f}".format(gps.longitude),
                altitud,
                speed,
                angle,
                satellites,
                gps.fix_quality, 
                # hdop,
                # geoid,
                sendinterval,
                "{:.0f}".format(battery_percentage)
            )


            print(packetforsend)
            # testpack = "test"

            # ## SEND
            LED.value = True
            rfm9x.send(bytes(packetforsend, "utf-8"))
            last_send = time.monotonic()
            LED.value = False

    except OSError:
        pass
    except RuntimeError:
        pass

@dewalex dewalex closed this as completed Feb 20, 2021
@jerryneedell
Copy link
Contributor

It looks like you have it working well for your application. I learned a lot from this discussion. Good luck with your project.

@dewalex
Copy link
Author

dewalex commented Feb 21, 2021

Thanks for your help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants