From a2e7d508ca52dd658a7977581ac44e2823d90420 Mon Sep 17 00:00:00 2001 From: Stefano Lande Date: Wed, 24 Aug 2022 18:08:50 +0200 Subject: [PATCH 01/10] implement telemetry report parsing --- .gitignore | 1 + aprslib/parsing/__init__.py | 44 +++++++++++++++------------- aprslib/parsing/telemetry.py | 44 ++++++++++++++++++++-------- docs/parse_formats.rst | 28 ++++++++++++++++++ tests/test_parse_telemetry_report.py | 22 ++++++++++++++ 5 files changed, 107 insertions(+), 32 deletions(-) create mode 100644 tests/test_parse_telemetry_report.py diff --git a/.gitignore b/.gitignore index 8e10410..856d786 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist *.egg-info env3 env2 +.idea diff --git a/aprslib/parsing/__init__.py b/aprslib/parsing/__init__.py index e8e37c4..fce7aae 100644 --- a/aprslib/parsing/__init__.py +++ b/aprslib/parsing/__init__.py @@ -45,25 +45,25 @@ def detect(x): from aprslib.parsing.weather import * unsupported_formats = { - '#':'raw weather report', - '$':'raw gps', - '%':'agrelo', - '&':'reserved', - '(':'unused', - ')':'item report', - '*':'complete weather report', - '+':'reserved', - '-':'unused', - '.':'reserved', - '<':'station capabilities', - '?':'general query format', - 'T':'telemetry report', - '[':'maidenhead locator beacon', - '\\':'unused', - ']':'unused', - '^':'unused', + '#': 'raw weather report', + '$': 'raw gps', + '%': 'agrelo', + '&': 'reserved', + '(': 'unused', + ')': 'item report', + '*': 'complete weather report', + '+': 'reserved', + '-': 'unused', + '.': 'reserved', + '<': 'station capabilities', + '?': 'general query format', + '[': 'maidenhead locator beacon', + '\\': 'unused', + ']': 'unused', + '^': 'unused', } + def _unicode_packet(packet): # attempt utf-8 try: @@ -114,7 +114,7 @@ def parse(packet): parsed = { 'raw': packet, - } + } # parse head try: @@ -149,7 +149,7 @@ def parse(packet): parsed.update({ 'format': 'beacon', 'text': packet_type + body, - }) + }) logger.debug("Parsed ok.") return parsed @@ -208,6 +208,10 @@ def _try_toparse_body(packet_type, body, parsed): body, result = parse_position(packet_type, body) + elif packet_type == "T": + logger.debug("Attempting to parse as telemetry report") + + body, result = parse_telemetry_report(body) + # we are done parsed.update(result) - diff --git a/aprslib/parsing/telemetry.py b/aprslib/parsing/telemetry.py index b3f35b5..ebe5b14 100644 --- a/aprslib/parsing/telemetry.py +++ b/aprslib/parsing/telemetry.py @@ -4,9 +4,10 @@ from aprslib.parsing import logger __all__ = [ - 'parse_comment_telemetry', - 'parse_telemetry_config', - ] + 'parse_comment_telemetry', + 'parse_telemetry_config', + 'parse_telemetry_report' +] def parse_comment_telemetry(text): @@ -23,19 +24,19 @@ def parse_comment_telemetry(text): temp = [0] * 7 for i in range(7): - temp[i] = base91.to_decimal(telemetry[i*2:i*2+2]) + temp[i] = base91.to_decimal(telemetry[i * 2:i * 2 + 2]) parsed.update({ 'telemetry': { 'seq': temp[0], 'vals': temp[1:6] - } - }) + } + }) if temp[6] != '': parsed['telemetry'].update({ 'bits': "{0:08b}".format(temp[6] & 0xFF)[::-1] - }) + }) return (text, parsed) @@ -62,14 +63,14 @@ def parse_telemetry_config(body): parsed.update({ 't%s' % form: defvals - }) + }) elif form == "EQNS": eqns = body.rstrip().split(',')[:15] teqns = [0, 1, 0] * 5 for idx, val in enumerate(eqns): if not re.match(r"^([-]?\d*\.?\d+|)$", val): - raise ParseError("value at %d is not a number in %s" % (idx+1, form)) + raise ParseError("value at %d is not a number in %s" % (idx + 1, form)) else: try: val = int(val) @@ -79,11 +80,11 @@ def parse_telemetry_config(body): teqns[idx] = val # group values in 5 list of 3 - teqns = [teqns[i*3:(i+1)*3] for i in range(5)] + teqns = [teqns[i * 3:(i + 1) * 3] for i in range(5)] parsed.update({ 't%s' % form: teqns - }) + }) elif form == "BITS": match = re.findall(r"^([01]{8}),(.{0,23})$", body.rstrip()) if not match: @@ -94,7 +95,26 @@ def parse_telemetry_config(body): parsed.update({ 't%s' % form: bits, 'title': title.strip(' ') - }) + }) return (body, parsed) + +def parse_telemetry_report(text): + temp = text.split(",") + parsed = {} + + if len(temp) == 7: + + seq = int(temp[0].replace('#', '')) + values = list(map(float, temp[1:6])) + + parsed.update({ + 'telemetry': { + 'seq': seq, + 'vals': values, + 'bits': temp[6] + } + }) + + return '', parsed diff --git a/docs/parse_formats.rst b/docs/parse_formats.rst index abeb7d1..3c401e4 100644 --- a/docs/parse_formats.rst +++ b/docs/parse_formats.rst @@ -257,6 +257,32 @@ Regular 'to': u'TOCALL', 'via': ''} + +Telemetry report +----------------------- + + +.. code:: python + + >>> aprslib.parse("FROMCALL>APDW16,WIDE1-1,qAR,TOCALL:T#165,13.21,0.39,5.10,14.94,36.12,11111100") + + { + 'raw': 'FROMCALL>APDW16,WIDE1-1,qAR,TOCALL:T#165,13.21,0.39,5.10,14.94,36.12,11111100', + 'from': 'FROMCALL', + 'to': 'APDW16', + 'path': ['WIDE1-1', 'qAR', 'TOCALL'], + 'via': 'TOCALL', + 'telemetry': + { + 'seq': 165, + 'vals': [13.21, 0.39, 5.1, 14.94, 36.12], + 'bits': '11111100' + }, + 'format': 'beacon', + 'text': 'T#165,13.21,0.39,5.10,14.94,36.12,11111100' + } + + Telemetry configuration ----------------------- @@ -296,3 +322,5 @@ Telemetry configuration 'to': 'TOCALL', 'via': ''} + + diff --git a/tests/test_parse_telemetry_report.py b/tests/test_parse_telemetry_report.py new file mode 100644 index 0000000..a2e9e4b --- /dev/null +++ b/tests/test_parse_telemetry_report.py @@ -0,0 +1,22 @@ +import unittest + +from aprslib.parsing import parse_telemetry_report + + +class ParseTelemetryReport(unittest.TestCase): + def setUp(self): + self.maxDiff = None + + def test_valid_telemetry_report(self): + packet = "#111,13.64,0.37,5.10,16.96,33.38,11110000" + expected = {'telemetry': + {'bits': '11110000', + 'seq': 111, + 'vals': [13.64, 0.37, 5.1, 16.96, 33.38]}} + + _, result = parse_telemetry_report(packet) + self.assertEqual(expected, result) + + +if __name__ == '__main__': + unittest.main() From f86c36f8fdb343c598bd8020cfe00c2a9131a9e1 Mon Sep 17 00:00:00 2001 From: Stefano Lande Date: Thu, 25 Aug 2022 16:07:48 +0200 Subject: [PATCH 02/10] improve handling of bad telemetry packets --- aprslib/parsing/telemetry.py | 14 +++++++++++--- docs/parse_formats.rst | 2 +- tests/test_parse_telemetry_report.py | 19 ++++++++++++++----- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/aprslib/parsing/telemetry.py b/aprslib/parsing/telemetry.py index ebe5b14..50ecca7 100644 --- a/aprslib/parsing/telemetry.py +++ b/aprslib/parsing/telemetry.py @@ -101,10 +101,16 @@ def parse_telemetry_config(body): def parse_telemetry_report(text): - temp = text.split(",") parsed = {} + rest = "" - if len(temp) == 7: + match = re.findall("(^#\d{3},(\d+(\.\d+)?,){5}[01]{8}$)", text) + + if match: + logger.debug("Attempting to parse telemetry-message packet") + + temp = text.split(",") + parsed.update({'format': 'telemetry-report'}) seq = int(temp[0].replace('#', '')) values = list(map(float, temp[1:6])) @@ -116,5 +122,7 @@ def parse_telemetry_report(text): 'bits': temp[6] } }) + else: + rest = text - return '', parsed + return rest, parsed diff --git a/docs/parse_formats.rst b/docs/parse_formats.rst index 3c401e4..5b29541 100644 --- a/docs/parse_formats.rst +++ b/docs/parse_formats.rst @@ -278,7 +278,7 @@ Telemetry report 'vals': [13.21, 0.39, 5.1, 14.94, 36.12], 'bits': '11111100' }, - 'format': 'beacon', + 'format': 'telemetry-report', 'text': 'T#165,13.21,0.39,5.10,14.94,36.12,11111100' } diff --git a/tests/test_parse_telemetry_report.py b/tests/test_parse_telemetry_report.py index a2e9e4b..3f48ea5 100644 --- a/tests/test_parse_telemetry_report.py +++ b/tests/test_parse_telemetry_report.py @@ -8,15 +8,24 @@ def setUp(self): self.maxDiff = None def test_valid_telemetry_report(self): - packet = "#111,13.64,0.37,5.10,16.96,33.38,11110000" - expected = {'telemetry': - {'bits': '11110000', - 'seq': 111, - 'vals': [13.64, 0.37, 5.1, 16.96, 33.38]}} + packet = "#111,13.64,0.37,5,16.96,33.38,11110000" + expected = {'format': 'telemetry-report', + 'telemetry': + {'bits': '11110000', + 'seq': 111, + 'vals': [13.64, 0.37, 5, 16.96, 33.38]}} _, result = parse_telemetry_report(packet) self.assertEqual(expected, result) + def test_invalid_telemetry_report(self): + packet = "#111a,Nan,0.37,5.10,16.96,33.38,11110000" + expected = {} + + rest, result = parse_telemetry_report(packet) + self.assertEqual(expected, result) + self.assertEqual(rest,packet) + if __name__ == '__main__': unittest.main() From 0a57535d5f7758df50e7bba3eae776e222c926fe Mon Sep 17 00:00:00 2001 From: Stefano Lande Date: Wed, 21 Sep 2022 09:44:09 +0200 Subject: [PATCH 03/10] remove unnecessary changes --- aprslib/parsing/__init__.py | 37 ++++++++++++++++++------------------ aprslib/parsing/telemetry.py | 18 +++++++++--------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/aprslib/parsing/__init__.py b/aprslib/parsing/__init__.py index fce7aae..b4f0b73 100644 --- a/aprslib/parsing/__init__.py +++ b/aprslib/parsing/__init__.py @@ -45,25 +45,24 @@ def detect(x): from aprslib.parsing.weather import * unsupported_formats = { - '#': 'raw weather report', - '$': 'raw gps', - '%': 'agrelo', - '&': 'reserved', - '(': 'unused', - ')': 'item report', - '*': 'complete weather report', - '+': 'reserved', - '-': 'unused', - '.': 'reserved', - '<': 'station capabilities', - '?': 'general query format', - '[': 'maidenhead locator beacon', - '\\': 'unused', - ']': 'unused', - '^': 'unused', + '#':'raw weather report', + '$':'raw gps', + '%':'agrelo', + '&':'reserved', + '(':'unused', + ')':'item report', + '*':'complete weather report', + '+':'reserved', + '-':'unused', + '.':'reserved', + '<':'station capabilities', + '?':'general query format', + '[':'maidenhead locator beacon', + '\\':'unused', + ']':'unused', + '^':'unused', } - def _unicode_packet(packet): # attempt utf-8 try: @@ -114,7 +113,7 @@ def parse(packet): parsed = { 'raw': packet, - } + } # parse head try: @@ -149,7 +148,7 @@ def parse(packet): parsed.update({ 'format': 'beacon', 'text': packet_type + body, - }) + }) logger.debug("Parsed ok.") return parsed diff --git a/aprslib/parsing/telemetry.py b/aprslib/parsing/telemetry.py index 50ecca7..ccd7c60 100644 --- a/aprslib/parsing/telemetry.py +++ b/aprslib/parsing/telemetry.py @@ -24,19 +24,19 @@ def parse_comment_telemetry(text): temp = [0] * 7 for i in range(7): - temp[i] = base91.to_decimal(telemetry[i * 2:i * 2 + 2]) + temp[i] = base91.to_decimal(telemetry[i*2:i*2+2]) parsed.update({ 'telemetry': { 'seq': temp[0], 'vals': temp[1:6] - } - }) + } + }) if temp[6] != '': parsed['telemetry'].update({ 'bits': "{0:08b}".format(temp[6] & 0xFF)[::-1] - }) + }) return (text, parsed) @@ -63,14 +63,14 @@ def parse_telemetry_config(body): parsed.update({ 't%s' % form: defvals - }) + }) elif form == "EQNS": eqns = body.rstrip().split(',')[:15] teqns = [0, 1, 0] * 5 for idx, val in enumerate(eqns): if not re.match(r"^([-]?\d*\.?\d+|)$", val): - raise ParseError("value at %d is not a number in %s" % (idx + 1, form)) + raise ParseError("value at %d is not a number in %s" % (idx+1, form)) else: try: val = int(val) @@ -80,11 +80,11 @@ def parse_telemetry_config(body): teqns[idx] = val # group values in 5 list of 3 - teqns = [teqns[i * 3:(i + 1) * 3] for i in range(5)] + teqns = [teqns[i*3:(i+1)*3] for i in range(5)] parsed.update({ 't%s' % form: teqns - }) + }) elif form == "BITS": match = re.findall(r"^([01]{8}),(.{0,23})$", body.rstrip()) if not match: @@ -95,7 +95,7 @@ def parse_telemetry_config(body): parsed.update({ 't%s' % form: bits, 'title': title.strip(' ') - }) + }) return (body, parsed) From 09ac00219b7d6a380b683b61bace1c7df904e125 Mon Sep 17 00:00:00 2001 From: Stefano Lande Date: Wed, 21 Sep 2022 10:01:15 +0200 Subject: [PATCH 04/10] fix deprecation warning --- aprslib/parsing/telemetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aprslib/parsing/telemetry.py b/aprslib/parsing/telemetry.py index ccd7c60..1bb8fcc 100644 --- a/aprslib/parsing/telemetry.py +++ b/aprslib/parsing/telemetry.py @@ -104,7 +104,7 @@ def parse_telemetry_report(text): parsed = {} rest = "" - match = re.findall("(^#\d{3},(\d+(\.\d+)?,){5}[01]{8}$)", text) + match = re.findall(r"(^#\d{3},(\d+(\.\d+)?,){5}[01]{8}$)", text) if match: logger.debug("Attempting to parse telemetry-message packet") From d915878b3bffc5aa9461622d531bac3382647641 Mon Sep 17 00:00:00 2001 From: shackrat Date: Thu, 14 Sep 2023 11:19:18 -0400 Subject: [PATCH 05/10] Implement Implements: Fix issue 80: Weather packet decoding. #81 --- aprslib/parsing/common.py | 3 ++- aprslib/parsing/position.py | 13 ++++++++++--- aprslib/parsing/weather.py | 6 ++++-- tests/test_parse_common.py | 8 ++++---- tests/test_parse_position.py | 20 ++++++++++---------- tests/test_parse_weather_data.py | 4 ++-- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/aprslib/parsing/common.py b/aprslib/parsing/common.py index fa5a465..246e8d7 100644 --- a/aprslib/parsing/common.py +++ b/aprslib/parsing/common.py @@ -138,6 +138,7 @@ def parse_data_extentions(body): # course speed bearing nrq # Page 27 of the spec # format: 111/222/333/444text + # Speed is in mph match = re.findall(r"^([0-9 \.]{3})/([0-9 \.]{3})", body) if match: cse, spd = match[0] @@ -145,7 +146,7 @@ def parse_data_extentions(body): if cse.isdigit() and cse != "000": parsed.update({'course': int(cse) if 1 <= int(cse) <= 360 else 0}) if spd.isdigit() and spd != "000": - parsed.update({'speed': int(spd)*1.852}) + parsed.update({'speed': int(spd)}) # DF Report format # Page 29 of teh spec diff --git a/aprslib/parsing/position.py b/aprslib/parsing/position.py index e66c502..9960a55 100644 --- a/aprslib/parsing/position.py +++ b/aprslib/parsing/position.py @@ -63,10 +63,19 @@ def parse_position(packet_type, body): # attempt to parse winddir/speed # Page 92 of the spec body, result = parse_data_extentions(body) - parsed.update(result) + wind_speed = result.get("speed") + wind_direction = result.get("course") logger.debug("Attempting to parse weather report from comment") body, result = parse_weather_data(body) + if wind_speed: + result.update({ + 'wind_speed': wind_speed, + }) + if wind_direction: + result.update({ + 'wind_direction': wind_direction, + }) parsed.update({ 'comment': body.strip(' '), 'weather': result, @@ -204,5 +213,3 @@ def parse_normal(body): }) return (body, parsed) - - diff --git a/aprslib/parsing/weather.py b/aprslib/parsing/weather.py index add8db2..1c8cb8c 100644 --- a/aprslib/parsing/weather.py +++ b/aprslib/parsing/weather.py @@ -7,8 +7,10 @@ ] # constants -wind_multiplier = 0.44704 -rain_multiplier = 0.254 +# wind is in miles per hour +wind_multiplier = 1 +# Spec 1.1 Rain is in hundredths of an inch. +rain_multiplier = 0.01 key_map = { 'g': 'wind_gust', diff --git a/tests/test_parse_common.py b/tests/test_parse_common.py index 69c8d73..9f4ac08 100644 --- a/tests/test_parse_common.py +++ b/tests/test_parse_common.py @@ -253,7 +253,7 @@ def test_course_speed(self): self.assertEqual(remaining, '/text') self.assertEqual(parsed, { 'course': 123, - 'speed': 100*1.852, + 'speed': 100, }) def test_course_speed_spaces(self): @@ -305,7 +305,7 @@ def test_empty_bearing_nrq(self): self.assertEqual(remaining, 'text') self.assertEqual(parsed, { 'course': 111, - 'speed': 100*1.852, + 'speed': 100, }) body = "111/100/2 /33.text" @@ -314,7 +314,7 @@ def test_empty_bearing_nrq(self): self.assertEqual(remaining, 'text') self.assertEqual(parsed, { 'course': 111, - 'speed': 100*1.852, + 'speed': 100, }) def test_course_speed_bearing_nrq_empty_cse_speed(self): @@ -335,7 +335,7 @@ def test_course_speed_bearing_nrq(self): self.assertEqual(remaining, 'text') self.assertEqual(parsed, { 'course': 123, - 'speed': 100*1.852, + 'speed': 100, 'bearing': 234, 'nrq': 345, }) diff --git a/tests/test_parse_position.py b/tests/test_parse_position.py index 4a2287b..d8d2de9 100644 --- a/tests/test_parse_position.py +++ b/tests/test_parse_position.py @@ -30,7 +30,7 @@ def test_position_packet_only_weather_valid(self): 'wind_gust': 0.0, 'temperature': 18.88888888888889, 'rain_1h': 0.0, - 'rain_24h': 0.0 + 'rain_24h': 0.0, } } @@ -50,14 +50,14 @@ def test_position_packet_data_ext_and_weather_valid(self): 'symbol_table': '/', 'latitude': 49.05833333333333, 'longitude': -72.02916666666667, - 'course': 90, - 'speed': 1*1.852, 'comment': '...dUII', 'weather': { 'wind_gust': 0.0, 'temperature': 18.88888888888889, 'rain_1h': 0.0, - 'rain_24h': 0.0 + 'rain_24h': 0.0, + 'wind_direction': 90, + 'wind_speed': 1, } } @@ -77,13 +77,13 @@ def test_position_packet_optional_speed(self): 'symbol_table': '/', 'latitude': 49.05833333333333, 'longitude': -72.02916666666667, - 'course': 90, 'comment': '...dUII', 'weather': { 'wind_gust': 0.0, 'temperature': 18.88888888888889, 'rain_1h': 0.0, - 'rain_24h': 0.0 + 'rain_24h': 0.0, + 'wind_direction': 90 } } @@ -103,13 +103,13 @@ def test_position_packet_optional_course(self): 'symbol_table': '/', 'latitude': 49.05833333333333, 'longitude': -72.02916666666667, - 'speed': 1*1.852, 'comment': '...dUII', 'weather': { 'wind_gust': 0.0, 'temperature': 18.88888888888889, 'rain_1h': 0.0, - 'rain_24h': 0.0 + 'rain_24h': 0.0, + 'wind_speed': 1, } } @@ -153,13 +153,13 @@ def test_position_packet_optional_course(self): 'symbol_table': '/', 'latitude': 49.05833333333333, 'longitude': -72.02916666666667, - 'speed': 1*1.852, 'comment': '...dUII', 'weather': { 'wind_gust': 0.0, 'temperature': 18.88888888888889, 'rain_1h': 0.0, - 'rain_24h': 0.0 + 'rain_24h': 0.0, + 'wind_speed': 1, } } diff --git a/tests/test_parse_weather_data.py b/tests/test_parse_weather_data.py index 7793525..18f9aa3 100644 --- a/tests/test_parse_weather_data.py +++ b/tests/test_parse_weather_data.py @@ -3,8 +3,8 @@ from aprslib.parsing import parse_weather_data from aprslib.parsing import parse -wind_multiplier = 0.44704 -mm_multiplier = 0.254 +wind_multiplier = 1 +mm_multiplier = 0.010 class ParseCommentWeather(unittest.TestCase): def setUp(self): From 4e2f7c01e4fb930251e6843dec7afe38b91695c2 Mon Sep 17 00:00:00 2001 From: shackrat Date: Thu, 14 Sep 2023 21:56:28 -0400 Subject: [PATCH 06/10] Weather Fix & Mic-E Radio Parsing Fixes missing wind_speed output when reported wind speed is zero.. Adds parsing of Mic-E Radio types. --- aprslib/parsing/mice.py | 174 +++++++++++++++++++++++++++++++++++- aprslib/parsing/position.py | 7 ++ 2 files changed, 180 insertions(+), 1 deletion(-) diff --git a/aprslib/parsing/mice.py b/aprslib/parsing/mice.py index 64c03c1..509bc91 100644 --- a/aprslib/parsing/mice.py +++ b/aprslib/parsing/mice.py @@ -32,6 +32,167 @@ "000": "Emergency", } +# Mic-e known radio type prefix list +RADIO_TYPE_PREFIXES = [ '>', ']', '`', '\''] + +# Mic-e known radio make/model table +RADIO_MODELS = [ + { + 'p': '`', + 's': '(5', + 'b': 'Anytone', + 'm': 'D578UV', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '(8', + 'b': 'Anytone', + 'm': 'D878UV', + 't': 'HT' + }, + { + 'p': '\'', + 's': '|3', + 'b': 'Byonics', + 'm': 'TinyTrack3', + 't': 'Tracker' + }, + { + 'p': '\'', + 's': '|4', + 'b': 'Byonics', + 'm': 'TinyTrack4', + 't': 'Tracker' + }, + { + 'p': 'T', + 's': '^v', + 'b': 'HinzTec', + 'm': 'Anyfrog', + 't': 'Tracker' + }, + { + 'p': '>', + 's': 'v', + 'b': 'Kenwood', + 'm': 'TH-D7A(G)', + 't': 'HT' + }, + { + 'p': ']', + 's': '', + 'b': 'Kenwood', + 'm': 'TM-D700', + 't': 'Mobile' + }, + { + 'p': ']', + 's': '=', + 'b': 'Kenwood', + 'm': 'TM-D710', + 't': 'Mobile' + }, + { + 'p': '>', + 's': '=', + 'b': 'Kenwood', + 'm': 'TH-D72', + 't': 'HT' + }, + { + 'p': '>', + 's': '^', + 'b': 'Kenwood', + 'm': 'TH-D74', + 't': 'HT' + }, + { + 'p': '`', + 's': '_ ', + 'b': 'Yaesu', + 'm': 'VX-8', + 't': 'HT' + }, + { + 'p': '`', + 's': '_#', + 'b': 'Yaesu', + 'm': 'VX-8G', + 't': 'HT' + }, + { + 'p': '`', + 's': '_$', + 'b': 'Yaesu', + 'm': 'FT1D', + 't': 'HT' + }, + { + 'p': '`', + 's': '_(', + 'b': 'Yaesu', + 'm': 'FT2D', + 't': 'HT' + }, + { + 'p': '`', + 's': '_0', + 'b': 'Yaesu', + 'm': 'FT3D', + 't': 'HT' + }, + { + 'p': '`', + 's': '_3', + 'b': 'Yaesu', + 'm': 'FT5D(R)', + 't': 'HT' + }, + { + 'p': '`', + 's': '_)', + 'b': 'Yaesu', + 'm': 'FTM-100D', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '_"', + 'b': 'Yaesu', + 'm': 'FTM-350', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '_2', + 'b': 'Yaesu', + 'm': 'FTM-200DR', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '_1', + 'b': 'Yaesu', + 'm': 'FTM-300DR', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '_%', + 'b': 'Yaesu', + 'm': 'FTM-400DR', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '_4', + 'b': 'Yaesu', + 'm': 'FTM-500DR', + 't': 'Mobile' + } + ] + # Mic-encoded packet # # 'lllc/s$/......... Mic-E no message capability @@ -214,7 +375,18 @@ def parse_mice(dstcall, body): # parse DAO extention body = parse_dao(body, parsed) + # Look for Mic-Eradio data in the comment + # See http://www.aprs.org/aprs12/mic-e-types.txt for spec details + comment = body.strip(' ') + prefix = RADIO_TYPE_PREFIXES.index(comment[0]) + if (prefix): + # Find a matching suffix + for i in range(len(RADIO_MODELS)): + if comment[-(len(RADIO_MODELS[i]["s"])):] in RADIO_MODELS[i]["s"] and RADIO_TYPE_PREFIXES[prefix] == RADIO_MODELS[i]["p"]: + parsed.update({'mradio_brand': RADIO_MODELS[i]["b"], 'mradio_model': RADIO_MODELS[i]["m"], 'mradio_type': RADIO_MODELS[i]["t"]}) + comment = comment.lstrip(RADIO_MODELS[i]["p"]).rstrip(RADIO_MODELS[i]["s"]) + # rest is a comment - parsed.update({'comment': body.strip(' ')}) + parsed.update({'comment': comment.strip(' ')}) return ('', parsed) diff --git a/aprslib/parsing/position.py b/aprslib/parsing/position.py index 9960a55..cdc7b3e 100644 --- a/aprslib/parsing/position.py +++ b/aprslib/parsing/position.py @@ -72,6 +72,13 @@ def parse_position(packet_type, body): result.update({ 'wind_speed': wind_speed, }) + elif wind_direction: + # Since result.get("speed") now returns None if speed is 0, + # set wind speed as zero if wind direction has also been reported + # This is consistent with the behavior in 0.7.1 and earlier. + result.update({ + 'wind_speed': 0, + }) if wind_direction: result.update({ 'wind_direction': wind_direction, From 94dcc10c6777db688db3ddfff97611f7c63881d0 Mon Sep 17 00:00:00 2001 From: shackrat Date: Thu, 14 Sep 2023 22:18:51 -0400 Subject: [PATCH 07/10] Reset for MIC-E PR --- .DS_Store | Bin 0 -> 6148 bytes aprslib/parsing/__init__.py | 7 +- aprslib/parsing/common.py | 3 +- aprslib/parsing/mice.py | 174 +-------------------------- aprslib/parsing/position.py | 20 +-- aprslib/parsing/telemetry.py | 34 +----- aprslib/parsing/weather.py | 6 +- docs/parse_formats.rst | 28 ----- tests/test_parse_common.py | 8 +- tests/test_parse_position.py | 20 +-- tests/test_parse_telemetry_report.py | 31 ----- tests/test_parse_weather_data.py | 4 +- 12 files changed, 28 insertions(+), 307 deletions(-) create mode 100644 .DS_Store delete mode 100644 tests/test_parse_telemetry_report.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0', ']', '`', '\''] - -# Mic-e known radio make/model table -RADIO_MODELS = [ - { - 'p': '`', - 's': '(5', - 'b': 'Anytone', - 'm': 'D578UV', - 't': 'Mobile' - }, - { - 'p': '`', - 's': '(8', - 'b': 'Anytone', - 'm': 'D878UV', - 't': 'HT' - }, - { - 'p': '\'', - 's': '|3', - 'b': 'Byonics', - 'm': 'TinyTrack3', - 't': 'Tracker' - }, - { - 'p': '\'', - 's': '|4', - 'b': 'Byonics', - 'm': 'TinyTrack4', - 't': 'Tracker' - }, - { - 'p': 'T', - 's': '^v', - 'b': 'HinzTec', - 'm': 'Anyfrog', - 't': 'Tracker' - }, - { - 'p': '>', - 's': 'v', - 'b': 'Kenwood', - 'm': 'TH-D7A(G)', - 't': 'HT' - }, - { - 'p': ']', - 's': '', - 'b': 'Kenwood', - 'm': 'TM-D700', - 't': 'Mobile' - }, - { - 'p': ']', - 's': '=', - 'b': 'Kenwood', - 'm': 'TM-D710', - 't': 'Mobile' - }, - { - 'p': '>', - 's': '=', - 'b': 'Kenwood', - 'm': 'TH-D72', - 't': 'HT' - }, - { - 'p': '>', - 's': '^', - 'b': 'Kenwood', - 'm': 'TH-D74', - 't': 'HT' - }, - { - 'p': '`', - 's': '_ ', - 'b': 'Yaesu', - 'm': 'VX-8', - 't': 'HT' - }, - { - 'p': '`', - 's': '_#', - 'b': 'Yaesu', - 'm': 'VX-8G', - 't': 'HT' - }, - { - 'p': '`', - 's': '_$', - 'b': 'Yaesu', - 'm': 'FT1D', - 't': 'HT' - }, - { - 'p': '`', - 's': '_(', - 'b': 'Yaesu', - 'm': 'FT2D', - 't': 'HT' - }, - { - 'p': '`', - 's': '_0', - 'b': 'Yaesu', - 'm': 'FT3D', - 't': 'HT' - }, - { - 'p': '`', - 's': '_3', - 'b': 'Yaesu', - 'm': 'FT5D(R)', - 't': 'HT' - }, - { - 'p': '`', - 's': '_)', - 'b': 'Yaesu', - 'm': 'FTM-100D', - 't': 'Mobile' - }, - { - 'p': '`', - 's': '_"', - 'b': 'Yaesu', - 'm': 'FTM-350', - 't': 'Mobile' - }, - { - 'p': '`', - 's': '_2', - 'b': 'Yaesu', - 'm': 'FTM-200DR', - 't': 'Mobile' - }, - { - 'p': '`', - 's': '_1', - 'b': 'Yaesu', - 'm': 'FTM-300DR', - 't': 'Mobile' - }, - { - 'p': '`', - 's': '_%', - 'b': 'Yaesu', - 'm': 'FTM-400DR', - 't': 'Mobile' - }, - { - 'p': '`', - 's': '_4', - 'b': 'Yaesu', - 'm': 'FTM-500DR', - 't': 'Mobile' - } - ] - # Mic-encoded packet # # 'lllc/s$/......... Mic-E no message capability @@ -375,18 +214,7 @@ def parse_mice(dstcall, body): # parse DAO extention body = parse_dao(body, parsed) - # Look for Mic-Eradio data in the comment - # See http://www.aprs.org/aprs12/mic-e-types.txt for spec details - comment = body.strip(' ') - prefix = RADIO_TYPE_PREFIXES.index(comment[0]) - if (prefix): - # Find a matching suffix - for i in range(len(RADIO_MODELS)): - if comment[-(len(RADIO_MODELS[i]["s"])):] in RADIO_MODELS[i]["s"] and RADIO_TYPE_PREFIXES[prefix] == RADIO_MODELS[i]["p"]: - parsed.update({'mradio_brand': RADIO_MODELS[i]["b"], 'mradio_model': RADIO_MODELS[i]["m"], 'mradio_type': RADIO_MODELS[i]["t"]}) - comment = comment.lstrip(RADIO_MODELS[i]["p"]).rstrip(RADIO_MODELS[i]["s"]) - # rest is a comment - parsed.update({'comment': comment.strip(' ')}) + parsed.update({'comment': body.strip(' ')}) return ('', parsed) diff --git a/aprslib/parsing/position.py b/aprslib/parsing/position.py index cdc7b3e..e66c502 100644 --- a/aprslib/parsing/position.py +++ b/aprslib/parsing/position.py @@ -63,26 +63,10 @@ def parse_position(packet_type, body): # attempt to parse winddir/speed # Page 92 of the spec body, result = parse_data_extentions(body) - wind_speed = result.get("speed") - wind_direction = result.get("course") + parsed.update(result) logger.debug("Attempting to parse weather report from comment") body, result = parse_weather_data(body) - if wind_speed: - result.update({ - 'wind_speed': wind_speed, - }) - elif wind_direction: - # Since result.get("speed") now returns None if speed is 0, - # set wind speed as zero if wind direction has also been reported - # This is consistent with the behavior in 0.7.1 and earlier. - result.update({ - 'wind_speed': 0, - }) - if wind_direction: - result.update({ - 'wind_direction': wind_direction, - }) parsed.update({ 'comment': body.strip(' '), 'weather': result, @@ -220,3 +204,5 @@ def parse_normal(body): }) return (body, parsed) + + diff --git a/aprslib/parsing/telemetry.py b/aprslib/parsing/telemetry.py index 1bb8fcc..b3f35b5 100644 --- a/aprslib/parsing/telemetry.py +++ b/aprslib/parsing/telemetry.py @@ -4,10 +4,9 @@ from aprslib.parsing import logger __all__ = [ - 'parse_comment_telemetry', - 'parse_telemetry_config', - 'parse_telemetry_report' -] + 'parse_comment_telemetry', + 'parse_telemetry_config', + ] def parse_comment_telemetry(text): @@ -99,30 +98,3 @@ def parse_telemetry_config(body): return (body, parsed) - -def parse_telemetry_report(text): - parsed = {} - rest = "" - - match = re.findall(r"(^#\d{3},(\d+(\.\d+)?,){5}[01]{8}$)", text) - - if match: - logger.debug("Attempting to parse telemetry-message packet") - - temp = text.split(",") - parsed.update({'format': 'telemetry-report'}) - - seq = int(temp[0].replace('#', '')) - values = list(map(float, temp[1:6])) - - parsed.update({ - 'telemetry': { - 'seq': seq, - 'vals': values, - 'bits': temp[6] - } - }) - else: - rest = text - - return rest, parsed diff --git a/aprslib/parsing/weather.py b/aprslib/parsing/weather.py index 1c8cb8c..add8db2 100644 --- a/aprslib/parsing/weather.py +++ b/aprslib/parsing/weather.py @@ -7,10 +7,8 @@ ] # constants -# wind is in miles per hour -wind_multiplier = 1 -# Spec 1.1 Rain is in hundredths of an inch. -rain_multiplier = 0.01 +wind_multiplier = 0.44704 +rain_multiplier = 0.254 key_map = { 'g': 'wind_gust', diff --git a/docs/parse_formats.rst b/docs/parse_formats.rst index 5b29541..abeb7d1 100644 --- a/docs/parse_formats.rst +++ b/docs/parse_formats.rst @@ -257,32 +257,6 @@ Regular 'to': u'TOCALL', 'via': ''} - -Telemetry report ------------------------ - - -.. code:: python - - >>> aprslib.parse("FROMCALL>APDW16,WIDE1-1,qAR,TOCALL:T#165,13.21,0.39,5.10,14.94,36.12,11111100") - - { - 'raw': 'FROMCALL>APDW16,WIDE1-1,qAR,TOCALL:T#165,13.21,0.39,5.10,14.94,36.12,11111100', - 'from': 'FROMCALL', - 'to': 'APDW16', - 'path': ['WIDE1-1', 'qAR', 'TOCALL'], - 'via': 'TOCALL', - 'telemetry': - { - 'seq': 165, - 'vals': [13.21, 0.39, 5.1, 14.94, 36.12], - 'bits': '11111100' - }, - 'format': 'telemetry-report', - 'text': 'T#165,13.21,0.39,5.10,14.94,36.12,11111100' - } - - Telemetry configuration ----------------------- @@ -322,5 +296,3 @@ Telemetry configuration 'to': 'TOCALL', 'via': ''} - - diff --git a/tests/test_parse_common.py b/tests/test_parse_common.py index 9f4ac08..69c8d73 100644 --- a/tests/test_parse_common.py +++ b/tests/test_parse_common.py @@ -253,7 +253,7 @@ def test_course_speed(self): self.assertEqual(remaining, '/text') self.assertEqual(parsed, { 'course': 123, - 'speed': 100, + 'speed': 100*1.852, }) def test_course_speed_spaces(self): @@ -305,7 +305,7 @@ def test_empty_bearing_nrq(self): self.assertEqual(remaining, 'text') self.assertEqual(parsed, { 'course': 111, - 'speed': 100, + 'speed': 100*1.852, }) body = "111/100/2 /33.text" @@ -314,7 +314,7 @@ def test_empty_bearing_nrq(self): self.assertEqual(remaining, 'text') self.assertEqual(parsed, { 'course': 111, - 'speed': 100, + 'speed': 100*1.852, }) def test_course_speed_bearing_nrq_empty_cse_speed(self): @@ -335,7 +335,7 @@ def test_course_speed_bearing_nrq(self): self.assertEqual(remaining, 'text') self.assertEqual(parsed, { 'course': 123, - 'speed': 100, + 'speed': 100*1.852, 'bearing': 234, 'nrq': 345, }) diff --git a/tests/test_parse_position.py b/tests/test_parse_position.py index d8d2de9..4a2287b 100644 --- a/tests/test_parse_position.py +++ b/tests/test_parse_position.py @@ -30,7 +30,7 @@ def test_position_packet_only_weather_valid(self): 'wind_gust': 0.0, 'temperature': 18.88888888888889, 'rain_1h': 0.0, - 'rain_24h': 0.0, + 'rain_24h': 0.0 } } @@ -50,14 +50,14 @@ def test_position_packet_data_ext_and_weather_valid(self): 'symbol_table': '/', 'latitude': 49.05833333333333, 'longitude': -72.02916666666667, + 'course': 90, + 'speed': 1*1.852, 'comment': '...dUII', 'weather': { 'wind_gust': 0.0, 'temperature': 18.88888888888889, 'rain_1h': 0.0, - 'rain_24h': 0.0, - 'wind_direction': 90, - 'wind_speed': 1, + 'rain_24h': 0.0 } } @@ -77,13 +77,13 @@ def test_position_packet_optional_speed(self): 'symbol_table': '/', 'latitude': 49.05833333333333, 'longitude': -72.02916666666667, + 'course': 90, 'comment': '...dUII', 'weather': { 'wind_gust': 0.0, 'temperature': 18.88888888888889, 'rain_1h': 0.0, - 'rain_24h': 0.0, - 'wind_direction': 90 + 'rain_24h': 0.0 } } @@ -103,13 +103,13 @@ def test_position_packet_optional_course(self): 'symbol_table': '/', 'latitude': 49.05833333333333, 'longitude': -72.02916666666667, + 'speed': 1*1.852, 'comment': '...dUII', 'weather': { 'wind_gust': 0.0, 'temperature': 18.88888888888889, 'rain_1h': 0.0, - 'rain_24h': 0.0, - 'wind_speed': 1, + 'rain_24h': 0.0 } } @@ -153,13 +153,13 @@ def test_position_packet_optional_course(self): 'symbol_table': '/', 'latitude': 49.05833333333333, 'longitude': -72.02916666666667, + 'speed': 1*1.852, 'comment': '...dUII', 'weather': { 'wind_gust': 0.0, 'temperature': 18.88888888888889, 'rain_1h': 0.0, - 'rain_24h': 0.0, - 'wind_speed': 1, + 'rain_24h': 0.0 } } diff --git a/tests/test_parse_telemetry_report.py b/tests/test_parse_telemetry_report.py deleted file mode 100644 index 3f48ea5..0000000 --- a/tests/test_parse_telemetry_report.py +++ /dev/null @@ -1,31 +0,0 @@ -import unittest - -from aprslib.parsing import parse_telemetry_report - - -class ParseTelemetryReport(unittest.TestCase): - def setUp(self): - self.maxDiff = None - - def test_valid_telemetry_report(self): - packet = "#111,13.64,0.37,5,16.96,33.38,11110000" - expected = {'format': 'telemetry-report', - 'telemetry': - {'bits': '11110000', - 'seq': 111, - 'vals': [13.64, 0.37, 5, 16.96, 33.38]}} - - _, result = parse_telemetry_report(packet) - self.assertEqual(expected, result) - - def test_invalid_telemetry_report(self): - packet = "#111a,Nan,0.37,5.10,16.96,33.38,11110000" - expected = {} - - rest, result = parse_telemetry_report(packet) - self.assertEqual(expected, result) - self.assertEqual(rest,packet) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_parse_weather_data.py b/tests/test_parse_weather_data.py index 18f9aa3..7793525 100644 --- a/tests/test_parse_weather_data.py +++ b/tests/test_parse_weather_data.py @@ -3,8 +3,8 @@ from aprslib.parsing import parse_weather_data from aprslib.parsing import parse -wind_multiplier = 1 -mm_multiplier = 0.010 +wind_multiplier = 0.44704 +mm_multiplier = 0.254 class ParseCommentWeather(unittest.TestCase): def setUp(self): From 3926627b3957f6377d892f923d194d28e29e13ab Mon Sep 17 00:00:00 2001 From: shackrat Date: Thu, 14 Sep 2023 22:22:44 -0400 Subject: [PATCH 08/10] Mic-E Radio Parsing & Comment Cleanup Accomplishes 2 tasks.. 1. Correctly parses APRS radio information (when present) in the Mic-E data. This is per http://www.aprs.org/aprs12/mic-e-types.txt 2. After successfully parsing radio information, it is removed from the comment. This has the effect of removing extraneous characters from the comment field. (Issue #67) --- aprslib/parsing/mice.py | 175 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/aprslib/parsing/mice.py b/aprslib/parsing/mice.py index 64c03c1..c7ab356 100644 --- a/aprslib/parsing/mice.py +++ b/aprslib/parsing/mice.py @@ -32,6 +32,168 @@ "000": "Emergency", } + +# Mic-e known radio type prefix list +RADIO_TYPE_PREFIXES = [ '>', ']', '`', '\''] + +# Mic-e known radio make/model table +RADIO_MODELS = [ + { + 'p': '`', + 's': '(5', + 'b': 'Anytone', + 'm': 'D578UV', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '(8', + 'b': 'Anytone', + 'm': 'D878UV', + 't': 'HT' + }, + { + 'p': '\'', + 's': '|3', + 'b': 'Byonics', + 'm': 'TinyTrack3', + 't': 'Tracker' + }, + { + 'p': '\'', + 's': '|4', + 'b': 'Byonics', + 'm': 'TinyTrack4', + 't': 'Tracker' + }, + { + 'p': 'T', + 's': '^v', + 'b': 'HinzTec', + 'm': 'Anyfrog', + 't': 'Tracker' + }, + { + 'p': '>', + 's': 'v', + 'b': 'Kenwood', + 'm': 'TH-D7A(G)', + 't': 'HT' + }, + { + 'p': ']', + 's': '', + 'b': 'Kenwood', + 'm': 'TM-D700', + 't': 'Mobile' + }, + { + 'p': ']', + 's': '=', + 'b': 'Kenwood', + 'm': 'TM-D710', + 't': 'Mobile' + }, + { + 'p': '>', + 's': '=', + 'b': 'Kenwood', + 'm': 'TH-D72', + 't': 'HT' + }, + { + 'p': '>', + 's': '^', + 'b': 'Kenwood', + 'm': 'TH-D74', + 't': 'HT' + }, + { + 'p': '`', + 's': '_ ', + 'b': 'Yaesu', + 'm': 'VX-8', + 't': 'HT' + }, + { + 'p': '`', + 's': '_#', + 'b': 'Yaesu', + 'm': 'VX-8G', + 't': 'HT' + }, + { + 'p': '`', + 's': '_$', + 'b': 'Yaesu', + 'm': 'FT1D', + 't': 'HT' + }, + { + 'p': '`', + 's': '_(', + 'b': 'Yaesu', + 'm': 'FT2D', + 't': 'HT' + }, + { + 'p': '`', + 's': '_0', + 'b': 'Yaesu', + 'm': 'FT3D', + 't': 'HT' + }, + { + 'p': '`', + 's': '_3', + 'b': 'Yaesu', + 'm': 'FT5D(R)', + 't': 'HT' + }, + { + 'p': '`', + 's': '_)', + 'b': 'Yaesu', + 'm': 'FTM-100D', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '_"', + 'b': 'Yaesu', + 'm': 'FTM-350', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '_2', + 'b': 'Yaesu', + 'm': 'FTM-200DR', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '_1', + 'b': 'Yaesu', + 'm': 'FTM-300DR', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '_%', + 'b': 'Yaesu', + 'm': 'FTM-400DR', + 't': 'Mobile' + }, + { + 'p': '`', + 's': '_4', + 'b': 'Yaesu', + 'm': 'FTM-500DR', + 't': 'Mobile' + } + ] + # Mic-encoded packet # # 'lllc/s$/......... Mic-E no message capability @@ -214,7 +376,18 @@ def parse_mice(dstcall, body): # parse DAO extention body = parse_dao(body, parsed) + # Look for Mic-E radio data in the comment + # See http://www.aprs.org/aprs12/mic-e-types.txt for spec details + comment = body.strip(' ') + prefix = RADIO_TYPE_PREFIXES.index(comment[0]) + if (prefix): + # Find a matching suffix + for i in range(len(RADIO_MODELS)): + if comment[-(len(RADIO_MODELS[i]["s"])):] in RADIO_MODELS[i]["s"] and RADIO_TYPE_PREFIXES[prefix] == RADIO_MODELS[i]["p"]: + parsed.update({'mradio_brand': RADIO_MODELS[i]["b"], 'mradio_model': RADIO_MODELS[i]["m"], 'mradio_type': RADIO_MODELS[i]["t"]}) + comment = comment.lstrip(RADIO_MODELS[i]["p"]).rstrip(RADIO_MODELS[i]["s"]) + # rest is a comment - parsed.update({'comment': body.strip(' ')}) + parsed.update({'comment': comment.strip(' ')}) return ('', parsed) From 4960c1a7575736c041eb16281ebb3fc753bd0eb7 Mon Sep 17 00:00:00 2001 From: shackrat Date: Thu, 14 Sep 2023 22:25:19 -0400 Subject: [PATCH 09/10] Delete .DS_Store --- .DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Thu, 14 Sep 2023 22:26:20 -0400 Subject: [PATCH 10/10] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 856d786..8e10410 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,3 @@ dist *.egg-info env3 env2 -.idea