jgromes / RadioLib

Universal wireless communication library for embedded devices
https://jgromes.github.io/RadioLib/
MIT License
1.59k stars 397 forks source link

Receiving APRS #569

Open sebastian-king opened 2 years ago

sebastian-king commented 2 years ago

I am interested in receiving APRS data using RadioLib, however, I am struggling to figure out how to do so. The APRSClient only provides methods for sending packets, not receiving them.

I am using the following code to try to receive an APRS packet, however, even though my handheld radio is receiving APRS just fine, my SX1276 module does not show any received packets. I am able to transmit ax.25 packets without issue.

#include <RadioLib.h>
#include "utilities.h"
#include "boards.h"

SX1276 radio = new Module(RADIO_CS_PIN, RADIO_DI0_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN);

void setup() {
  Serial.begin(9600);

  // initialize SX1276 with default settings
  Serial.print(F("[SX1276] Initializing ... "));
  int state = radio.beginFSK(144.39, 1.2);
  if (state == RADIOLIB_ERR_NONE) {
    Serial.println(F("success!"));
  } else {
    Serial.print(F("failed, code "));
    Serial.println(state);
    while (true);
  }
}

void loop() {
  Serial.print(F("[SX1276] Waiting for incoming transmission ... "));

  String str;
  int state = radio.receive(str);

  if (state == RADIOLIB_ERR_NONE) {
    // packet was successfully received
    Serial.println(F("success!"));

    // print the data of the packet
    Serial.print(F("[SX1276] Data:\t\t\t"));
    Serial.println(str);

    // print the RSSI (Received Signal Strength Indicator)
    // of the last received packet
    Serial.print(F("[SX1276] RSSI:\t\t\t"));
    Serial.print(radio.getRSSI());
    Serial.println(F(" dBm"));

    // print the SNR (Signal-to-Noise Ratio)
    // of the last received packet
    Serial.print(F("[SX1276] SNR:\t\t\t"));
    Serial.print(radio.getSNR());
    Serial.println(F(" dB"));

    // print frequency error
    // of the last received packet
    Serial.print(F("[SX1276] Frequency error:\t"));
    Serial.print(radio.getFrequencyError());
    Serial.println(F(" Hz"));

  } else if (state == RADIOLIB_ERR_RX_TIMEOUT) {
    // timeout occurred while waiting for a packet
    Serial.println(F("timeout!"));

  } else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
    // packet was received, but is malformed
    Serial.println(F("CRC error!"));

  } else {
    // some other error occurred
    Serial.print(F("failed, code "));
    Serial.println(state);

  }
}

If it helps, I am using a TTGO T-Beam (V1.1).

jgromes commented 2 years ago

At the moment, RadioLib does not support receiving APRS and most other HAM modes like RTTY. This is because even transmitting it is achieved by a massive hack (basically by using the transceiver to only send a carrier and directly modulating that).

I recently added Morse code reception, and the same mechanism used for that might be usable to receive AFSK signal (and so APRS). Basically by putting the module to a special mode in which it outputs demodulated data on one of its pins, and so AFSK tones become a square wave signal. However, it's a big question whether the processing of the AFSK signal can be made reliable enough. And even bigger question is - is it worth it? I can easily imagine a use case for APRS transmissions using RadioLib - in microcontroller-based trackers. However, less so for receiving - why build microcontroller-based repeaters or IGates?

jgromes commented 2 years ago

@sebastian-king I did some experiments, and it looks like could be possible using the direct mode reception. In this mode, the radio outputs the demodulated data as digital signals on two of its pins (one is data, the other is clock). With that, the AFSK audio signal becomes digital waveform. The high and low tones are clearly visible as 1200/2200 Hz. This signal can then be sampled by using the clock signal as a source for interrupt to read the data line.

The result is an array of bits representing the AFSK signal, like this:

01010011001100110011001100110101010101010101010101010101010011010101010101010101010101010011000101010101010101010101010011010101

01010 is a 2200 Hz tone, while 00110011 is a 1200 Hz tone. In the above, there's the encoded AX.25 preamble starting with bytes 0x7E 0x01 0x01 etc.

However, there's going to be significant signal processing required - for example, due to slight timing errors, it's not as simple as e.g. taking every 4 bits to resolve the tones.

TL;DR - It's possible, but not straightforward and will likely require manual tuning.

DeflateAwning commented 1 year ago

Is there any plan to support this or add its implementation?

jgromes commented 1 year ago

@DeflateAwning yes, there is - as per my previous comment, it is turning out to be a bit more tricky, so if anyone can provide an algorithm for demodulating AFSK in the form of binary strings, let me know ;)

DeflateAwning commented 1 year ago

Would love to try to help! Need this for another project!

Is there docs on what the format/encoding of those binary strings are? How did you find out that "01010 is a 2200 Hz tone, while 00110011 is a 1200 Hz tone"? In applying that pattern to the bit array you had there, it appears that there are lots of bits that don't fit that encoding scheme; any idea how they are supposed to fit in or where they come from?

jgromes commented 1 year ago

Is there docs on what the format/encoding of those binary strings are?

There's no format, it's just the result of sampling a digital pin that's coming from the radio. It's being sampled at 4400 Hz (twice the highest tone as needed by Nyqist theorem), so an alternating sequence of 01010... corrseponds to frequency of 2200 Hz. When there is enough time for the same level to be sapled twice (001100...), it means that the tone is about 1100 Hz.

it appears that there are lots of bits that don't fit that encoding scheme

That's exactly the problem, because 2200 divided by 1200 is not exactly two, there will be timing discrepancies if you try to just "count the bits". I tried implementing a simple correlating demodulator with local oscillators at 1200 and 2200 Hz just over the binary sequence, but it seems that's impossible (since the binary sequence would have a "DC offset" of 0.5, which doesn't make sense for a digital value). After the signal is demodulated, you would then have to extract the clock and only then start decoding.

DeflateAwning commented 1 year ago

I think my initial plan might be to sample at an even higher frequency than the Nyqist frequency to better read out the 2200 Hz symbols.

In the example in the original issue, do you know what the second arg to radio.beginFSK(144.39, 1.2) is?

Which pin were you sampling on the SX1276?

jgromes commented 1 year ago

I think my initial plan might be to sample at an even higher frequency

For most of my experiments, I was working at 26.4 kHz, since 26400 can be divided by both 1200 and 2200. Didn't work because of the aforementioned DC offset. I think the core issue is that to perform correlation with local oscillators, two-state samples are not enough, it would have to be at least -1/0/1.

In the example in the original issue, do you know what the second arg

Ignore the original, that was just the OPs attempt to guess the parameters. The third arguments is frequency deviation, which is only used for Tx. The bit rate determines the sampling frequency, os you should do something like beginFSK(434.0, 26.4).

Which pin were you sampling on the SX1276?

DIO2. You have to enable OOK and direct mode:

// the "sync word" are some AX.25 preamble bits at the set sampling rate
radio.setDirectSyncWord(0x3F03F03F, 32);
radio.setDirectAction(readBit);
radio.receiveDirect();

(...)

void readBit(void) {
  // read the DIO2 pin
  radio.readBit(4);
}

void loop() {
  // wait to get some data
  if(radio.available() > 22) {
    radio.standby();
    while(radio.available()) {
      uint8_t b = radio.read();
      // demodulate here
    }
    radio.receiveDirect();
  }
}
DeflateAwning commented 1 year ago

Can you explain why you chose OOK mode? I'm having trouble seeing the connection between AFSK and OOK

jgromes commented 1 year ago

@DeflateAwning hmm, it's been a while so I'm not entirely sure, I think that without it there was some issue with generating the digital output, but I don't recall what exactly it was.

DeflateAwning commented 1 year ago

Do you have the exact code snippet you used to generate the bitstream on Sep 18, 2022?

jgromes commented 1 year ago

@DeflateAwning I don't have the exact code, so I attached the last working version I found, which contains couple attempts at demodulation. Though I'm not sure if it will help you, the point is to take the received data and demodulate it. It doesn't matter much if the data is actually being received - the reception does work. The point is to take that binary string and turn it into 0x7E 0x01 0x01 0x01.

APRS_Receive.ino.txt

lyusupov commented 1 year ago

@jgromes

However, less so for receiving - why build microcontroller-based repeaters or IGates ?

There are microcontroller-based repeaters and IGates already, such as: 1) ESP32IGate

2) and APRS-ESP (which is actually a derivative of the ESP32IGate mentioned above)

There is also a "microcontroller-based" APRS transceiver, such as

3) SoftRF Ham Edition

All the projects above are using LibAPRS-ESP32 library that

jgromes commented 1 year ago

@lyusupov interesting projects, thanks! However, to me it doesn't answer the fundamental question, if this is really needed in RadioLib. And if so, then how to do it.

As far as I can tell, the LibAPRS-ESP32 library comes with significant caveats, mainly the dependency on the ESP32 platform and its I2S peripheral. In addition, all of the projects above use significantly different hardware than what RadioLib targets, so unfrontunately I don't think it's very useful even as an inspiration for implementing AX.25/APRS reception here.

lyusupov commented 1 year ago

dependency on the ESP32 and its I2S peripheral.

Not necessarily. As an example, the recent ESP32-S3 : 1) no longer has built-in DAC and 2) it's built-in ADC is no longer connected to I2S bus.

The LibAPRS-ESP32 for ESP32-S3 can

More details are available on this thread: https://github.com/erstec/APRS-ESP/discussions/29

jgromes commented 1 year ago

That's interesting, but still not what would need to be done for APRS reception in RadioLib. The fundamental issue is that the signal has already been demodulated by the transceiver. So what RadioLib reads is a serial digital input of 1s and 0s at the mark and space frequencies (as shown a few posts above), which has turned out to be quite tricky to further process, at least for me. Though I will admit DSP is not my strong suit, so I'm inviting anyone willing to take a shot at this :)