diff --git a/README.md b/README.md
index f6311c01..b73183bd 100644
--- a/README.md
+++ b/README.md
@@ -599,6 +599,11 @@ jradio has lots of built-in satellite decoders. Some of them have non standard d
52191, 52192, 52188 |
ru.r2cloud.jradio.suchai2.Suchai2Beacon |
+
+ GRBBETA |
+ 60237 |
+ ru.r2cloud.jradio.grbbeta.GRBBetaBeacon |
+
diff --git a/src/main/java/ru/r2cloud/jradio/grbbeta/GRBBetaBeacon.java b/src/main/java/ru/r2cloud/jradio/grbbeta/GRBBetaBeacon.java
new file mode 100644
index 00000000..04a69f5b
--- /dev/null
+++ b/src/main/java/ru/r2cloud/jradio/grbbeta/GRBBetaBeacon.java
@@ -0,0 +1,82 @@
+package ru.r2cloud.jradio.grbbeta;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import ru.r2cloud.jradio.Beacon;
+import ru.r2cloud.jradio.csp.Header;
+import ru.r2cloud.jradio.fec.ccsds.UncorrectableException;
+
+public class GRBBetaBeacon extends Beacon {
+
+ private ru.r2cloud.jradio.ax25.Header ax25Header;
+ private Header cspHeader;
+ private TrxBeacon trx;
+
+ private byte[] unknownPayload;
+ private String unknownMessage;
+
+ @Override
+ public void readBeacon(byte[] data) throws IOException, UncorrectableException {
+ try {
+ DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data));
+ ax25Header = new ru.r2cloud.jradio.ax25.Header(dis);
+ byte[] bodyBytes = new byte[dis.available()];
+ dis.readFully(bodyBytes);
+ String body = new String(bodyBytes, StandardCharsets.US_ASCII);
+ if (body.startsWith("U,")) {
+ trx = new TrxBeacon(body);
+ } else {
+ unknownMessage = body;
+ }
+ } catch (Exception e) {
+ DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data));
+ cspHeader = new Header(dis);
+ unknownPayload = new byte[dis.available()];
+ dis.readFully(unknownPayload);
+ }
+ }
+
+ public ru.r2cloud.jradio.ax25.Header getAx25Header() {
+ return ax25Header;
+ }
+
+ public void setAx25Header(ru.r2cloud.jradio.ax25.Header ax25Header) {
+ this.ax25Header = ax25Header;
+ }
+
+ public Header getCspHeader() {
+ return cspHeader;
+ }
+
+ public void setCspHeader(Header cspHeader) {
+ this.cspHeader = cspHeader;
+ }
+
+ public TrxBeacon getTrx() {
+ return trx;
+ }
+
+ public void setTrx(TrxBeacon trx) {
+ this.trx = trx;
+ }
+
+ public byte[] getUnknownPayload() {
+ return unknownPayload;
+ }
+
+ public void setUnknownPayload(byte[] unknownPayload) {
+ this.unknownPayload = unknownPayload;
+ }
+
+ public String getUnknownMessage() {
+ return unknownMessage;
+ }
+
+ public void setUnknownMessage(String unknownMessage) {
+ this.unknownMessage = unknownMessage;
+ }
+
+}
diff --git a/src/main/java/ru/r2cloud/jradio/grbbeta/TrxBeacon.java b/src/main/java/ru/r2cloud/jradio/grbbeta/TrxBeacon.java
new file mode 100644
index 00000000..558cb2af
--- /dev/null
+++ b/src/main/java/ru/r2cloud/jradio/grbbeta/TrxBeacon.java
@@ -0,0 +1,151 @@
+package ru.r2cloud.jradio.grbbeta;
+
+import java.util.regex.Pattern;
+
+public class TrxBeacon {
+ private static final Pattern COMMA = Pattern.compile(",");
+
+ private long uptimeSinceReset;
+ private long uptime;
+ private long radioBootCount;
+ private long rfSegmentResetCount;
+ private float radioMcuTemperature;
+ private float rfChipTemperature;
+ private float rfPowerAmplifierTemperature;
+ private long digipeaterMessageCount;
+ private String lastDigipeaterCallsign;
+ private long rxDataPackets;
+ private long txDataPackets;
+ private float actualRssi;
+ private float rssiPreamble;
+
+ public TrxBeacon() {
+ // do nothing
+ }
+
+ public TrxBeacon(String message) {
+ String[] parts = COMMA.split(message.trim());
+ int i = 1;
+ uptimeSinceReset = Long.valueOf(parts[i++]);
+ uptime = Long.valueOf(parts[i++]);
+ radioBootCount = Long.valueOf(parts[i++]);
+ rfSegmentResetCount = Long.valueOf(parts[i++]);
+ radioMcuTemperature = Long.valueOf(parts[i++]) * 0.01f;
+ rfChipTemperature = Long.valueOf(parts[i++]) * 0.01f;
+ rfPowerAmplifierTemperature = Long.valueOf(parts[i++]) * 0.01f;
+ digipeaterMessageCount = Long.valueOf(parts[i++]);
+ lastDigipeaterCallsign = parts[i++].trim();
+ if (lastDigipeaterCallsign.length() == 0) {
+ lastDigipeaterCallsign = null;
+ }
+ rxDataPackets = Long.valueOf(parts[i++]);
+ txDataPackets = Long.valueOf(parts[i++]);
+ actualRssi = Long.valueOf(parts[i++]) / 2.0f - 134;
+ rssiPreamble = Long.valueOf(parts[i++]) / 2.0f - 134;
+ }
+
+ public long getUptimeSinceReset() {
+ return uptimeSinceReset;
+ }
+
+ public void setUptimeSinceReset(long uptimeSinceReset) {
+ this.uptimeSinceReset = uptimeSinceReset;
+ }
+
+ public long getUptime() {
+ return uptime;
+ }
+
+ public void setUptime(long uptime) {
+ this.uptime = uptime;
+ }
+
+ public long getRadioBootCount() {
+ return radioBootCount;
+ }
+
+ public void setRadioBootCount(long radioBootCount) {
+ this.radioBootCount = radioBootCount;
+ }
+
+ public long getRfSegmentResetCount() {
+ return rfSegmentResetCount;
+ }
+
+ public void setRfSegmentResetCount(long rfSegmentResetCount) {
+ this.rfSegmentResetCount = rfSegmentResetCount;
+ }
+
+ public float getRadioMcuTemperature() {
+ return radioMcuTemperature;
+ }
+
+ public void setRadioMcuTemperature(float radioMcuTemperature) {
+ this.radioMcuTemperature = radioMcuTemperature;
+ }
+
+ public float getRfChipTemperature() {
+ return rfChipTemperature;
+ }
+
+ public void setRfChipTemperature(float rfChipTemperature) {
+ this.rfChipTemperature = rfChipTemperature;
+ }
+
+ public float getRfPowerAmplifierTemperature() {
+ return rfPowerAmplifierTemperature;
+ }
+
+ public void setRfPowerAmplifierTemperature(float rfPowerAmplifierTemperature) {
+ this.rfPowerAmplifierTemperature = rfPowerAmplifierTemperature;
+ }
+
+ public long getDigipeaterMessageCount() {
+ return digipeaterMessageCount;
+ }
+
+ public void setDigipeaterMessageCount(long digipeaterMessageCount) {
+ this.digipeaterMessageCount = digipeaterMessageCount;
+ }
+
+ public String getLastDigipeaterCallsign() {
+ return lastDigipeaterCallsign;
+ }
+
+ public void setLastDigipeaterCallsign(String lastDigipeaterCallsign) {
+ this.lastDigipeaterCallsign = lastDigipeaterCallsign;
+ }
+
+ public long getRxDataPackets() {
+ return rxDataPackets;
+ }
+
+ public void setRxDataPackets(long rxDataPackets) {
+ this.rxDataPackets = rxDataPackets;
+ }
+
+ public long getTxDataPackets() {
+ return txDataPackets;
+ }
+
+ public void setTxDataPackets(long txDataPackets) {
+ this.txDataPackets = txDataPackets;
+ }
+
+ public float getActualRssi() {
+ return actualRssi;
+ }
+
+ public void setActualRssi(float actualRssi) {
+ this.actualRssi = actualRssi;
+ }
+
+ public float getRssiPreamble() {
+ return rssiPreamble;
+ }
+
+ public void setRssiPreamble(float rssiPreamble) {
+ this.rssiPreamble = rssiPreamble;
+ }
+
+}
diff --git a/src/test/java/ru/r2cloud/jradio/grbbeta/GRBBetaBeaconTest.java b/src/test/java/ru/r2cloud/jradio/grbbeta/GRBBetaBeaconTest.java
new file mode 100644
index 00000000..714ba3ba
--- /dev/null
+++ b/src/test/java/ru/r2cloud/jradio/grbbeta/GRBBetaBeaconTest.java
@@ -0,0 +1,37 @@
+package ru.r2cloud.jradio.grbbeta;
+
+import static com.google.code.beanmatchers.BeanMatchers.hasValidBeanConstructor;
+import static com.google.code.beanmatchers.BeanMatchers.hasValidGettersAndSetters;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+
+import ru.r2cloud.jradio.AssertJson;
+import ru.r2cloud.jradio.fec.ViterbiTest;
+
+public class GRBBetaBeaconTest {
+
+ @Test
+ public void testBeacon() throws Exception {
+ byte[] data = ViterbiTest.hexStringToByteArray("86A240404040609082648EA4846103F0552C323633373233312C323635393030372C322C302C313237352C313237342C313231382C302C2C313830382C3237393833322C37352C393200");
+ GRBBetaBeacon result = new GRBBetaBeacon();
+ result.readBeacon(data);
+ AssertJson.assertObjectsEqual("GRBBetaBeaconTrx.json", result);
+ }
+
+ @Test
+ public void testCspBeacon() throws Exception {
+ byte[] data = ViterbiTest.hexStringToByteArray("4050EA3EB97297B4EAEC190B3621EE6447C69D532D");
+ GRBBetaBeacon result = new GRBBetaBeacon();
+ result.readBeacon(data);
+ AssertJson.assertObjectsEqual("GRBBetaBeaconCsp.json", result);
+ }
+
+ @Test
+ public void testPojo() {
+ assertThat(GRBBetaBeacon.class, allOf(hasValidBeanConstructor(), hasValidGettersAndSetters()));
+ assertThat(TrxBeacon.class, allOf(hasValidBeanConstructor(), hasValidGettersAndSetters()));
+ }
+
+}
diff --git a/src/test/resources/expected/GRBBetaBeaconCsp.json b/src/test/resources/expected/GRBBetaBeaconCsp.json
new file mode 100644
index 00000000..687ad795
--- /dev/null
+++ b/src/test/resources/expected/GRBBetaBeaconCsp.json
@@ -0,0 +1,36 @@
+{
+ "cspHeader": {
+ "priority": "CSP_PRIO_HIGH",
+ "source": 0,
+ "destination": 5,
+ "sourcePort": 42,
+ "destinationPort": 3,
+ "ffrag": true,
+ "fhmac": true,
+ "fxtea": true,
+ "frdp": true,
+ "fcrc32": false
+ },
+ "unknownPayload": [
+ -71,
+ 114,
+ -105,
+ -76,
+ -22,
+ -20,
+ 25,
+ 11,
+ 54,
+ 33,
+ -18,
+ 100,
+ 71,
+ -58,
+ -99,
+ 83,
+ 45
+ ],
+ "beginSample": 0,
+ "beginMillis": 0,
+ "endSample": 0
+}
\ No newline at end of file
diff --git a/src/test/resources/expected/GRBBetaBeaconTrx.json b/src/test/resources/expected/GRBBetaBeaconTrx.json
new file mode 100644
index 00000000..2cb0162b
--- /dev/null
+++ b/src/test/resources/expected/GRBBetaBeaconTrx.json
@@ -0,0 +1,36 @@
+{
+ "ax25Header": {
+ "destinationAddress": {
+ "callsign": "CQ",
+ "ssid": 0,
+ "extensionBit": 0
+ },
+ "sourceAddress": {
+ "callsign": "HA2GRB",
+ "ssid": 0,
+ "extensionBit": 1
+ },
+ "frameType": "U",
+ "sendSequenceNumber": 0,
+ "receiveSequenceNumber": 0,
+ "uControlType": "UI",
+ "pid": 240
+ },
+ "trx": {
+ "uptimeSinceReset": 2637231,
+ "uptime": 2659007,
+ "radioBootCount": 2,
+ "rfSegmentResetCount": 0,
+ "radioMcuTemperature": 12.75,
+ "rfChipTemperature": 12.74,
+ "rfPowerAmplifierTemperature": 12.179999,
+ "digipeaterMessageCount": 0,
+ "rxDataPackets": 1808,
+ "txDataPackets": 279832,
+ "actualRssi": -96.5,
+ "rssiPreamble": -88.0
+ },
+ "beginSample": 0,
+ "beginMillis": 0,
+ "endSample": 0
+}
\ No newline at end of file