diff --git a/csdr/chain/rtl433.py b/csdr/chain/rtl433.py
deleted file mode 100644
index e34c20cf..00000000
--- a/csdr/chain/rtl433.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from csdr.chain.demodulator import ServiceDemodulator, DialFrequencyReceiver
-from csdr.module.rtl433 import Rtl433Module
-from pycsdr.modules import Convert, Agc
-from pycsdr.types import Format
-from owrx.rtl433 import Rtl433Parser
-
-
-class Rtl433Demodulator(ServiceDemodulator, DialFrequencyReceiver):
- def __init__(self, service: bool = False):
- self.sampleRate = 48000
- self.parser = Rtl433Parser(service=service)
- workers = [
- Agc(Format.COMPLEX_FLOAT),
- Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT),
- Rtl433Module(self.sampleRate, jsonOutput = not service),
- self.parser,
- ]
- # Connect all the workers
- super().__init__(workers)
-
- def getFixedAudioRate(self) -> int:
- return self.sampleRate
-
- def supportsSquelch(self) -> bool:
- return False
-
- def setDialFrequency(self, frequency: int) -> None:
- self.parser.setDialFrequency(frequency)
-
diff --git a/csdr/chain/multimon.py b/csdr/chain/toolbox.py
similarity index 69%
rename from csdr/chain/multimon.py
rename to csdr/chain/toolbox.py
index 5029c763..099f1b5a 100644
--- a/csdr/chain/multimon.py
+++ b/csdr/chain/toolbox.py
@@ -1,8 +1,31 @@
from csdr.chain.demodulator import ServiceDemodulator, DialFrequencyReceiver
-from csdr.module.multimon import MultimonModule
-from pycsdr.modules import FmDemod, AudioResampler, Convert, Squelch
+from csdr.module.toolbox import Rtl433Module, MultimonModule
+from pycsdr.modules import FmDemod, AudioResampler, Convert, Agc, Squelch
from pycsdr.types import Format
-from owrx.multimon import MultimonParser, PageParser, SelCallParser
+from owrx.toolbox import TextParser, PageParser, SelCallParser, IsmParser
+
+
+class IsmDemodulator(ServiceDemodulator, DialFrequencyReceiver):
+ def __init__(self, service: bool = False):
+ self.sampleRate = 48000
+ self.parser = IsmParser(service=service)
+ workers = [
+ Agc(Format.COMPLEX_FLOAT),
+ Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT),
+ Rtl433Module(self.sampleRate, jsonOutput = not service),
+ self.parser,
+ ]
+ # Connect all the workers
+ super().__init__(workers)
+
+ def getFixedAudioRate(self) -> int:
+ return self.sampleRate
+
+ def supportsSquelch(self) -> bool:
+ return False
+
+ def setDialFrequency(self, frequency: int) -> None:
+ self.parser.setDialFrequency(frequency)
class MultimonDemodulator(ServiceDemodulator, DialFrequencyReceiver):
@@ -55,7 +78,7 @@ class PageDemodulator(MultimonDemodulator):
class EasDemodulator(MultimonDemodulator):
def __init__(self, service: bool = False):
- super().__init__(["EAS"], MultimonParser(service=service))
+ super().__init__(["EAS"], TextParser(service=service))
class SelCallDemodulator(MultimonDemodulator):
diff --git a/csdr/module/multimon.py b/csdr/module/multimon.py
deleted file mode 100644
index d1635cd0..00000000
--- a/csdr/module/multimon.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from pycsdr.types import Format
-from csdr.module import PopenModule
-
-
-class MultimonModule(PopenModule):
- def __init__(self, decoders: list[str]):
- self.decoders = decoders
- super().__init__()
-
- def getCommand(self):
- cmd = ["multimon-ng", "-", "-v0", "-c"]
- for x in self.decoders:
- cmd += ["-a", x]
- return cmd
-
- def getInputFormat(self) -> Format:
- return Format.SHORT
-
- def getOutputFormat(self) -> Format:
- return Format.CHAR
-
diff --git a/csdr/module/rtl433.py b/csdr/module/toolbox.py
similarity index 66%
rename from csdr/module/rtl433.py
rename to csdr/module/toolbox.py
index 1ac53570..2884e0ef 100644
--- a/csdr/module/rtl433.py
+++ b/csdr/module/toolbox.py
@@ -8,10 +8,10 @@ class Rtl433Module(PopenModule):
self.jsonOutput = jsonOutput
super().__init__()
- def getCommandTEST(self):
+ def getCommand(self):
return ["dummy433"]
- def getCommand(self):
+ def getCommandOK(self):
return [
"rtl_433", "-r", "cs16:-", "-s", str(self.sampleRate),
"-M", "time:utc", "-F", "json" if self.jsonOutput else "kv",
@@ -30,3 +30,21 @@ class Rtl433Module(PopenModule):
def getOutputFormat(self) -> Format:
return Format.CHAR
+
+class MultimonModule(PopenModule):
+ def __init__(self, decoders: list[str]):
+ self.decoders = decoders
+ super().__init__()
+
+ def getCommand(self):
+ cmd = ["multimon-ng", "-", "-v0", "-c"]
+ for x in self.decoders:
+ cmd += ["-a", x]
+ return cmd
+
+ def getInputFormat(self) -> Format:
+ return Format.SHORT
+
+ def getOutputFormat(self) -> Format:
+ return Format.CHAR
+
diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js
index 621e3a36..e0441a28 100644
--- a/htdocs/lib/MessagePanel.js
+++ b/htdocs/lib/MessagePanel.js
@@ -285,7 +285,7 @@ PageMessagePanel.prototype.render = function() {
$(this.el).append($(
'
' +
'' +
- '| Paging | ' +
+ 'Pager Messages | ' +
'
' +
'' +
'
'
@@ -347,7 +347,7 @@ IsmMessagePanel.prototype.render = function() {
$(this.el).append($(
'' +
'' +
- '| Devices | ' +
+ 'Device Messages | ' +
'
' +
'' +
'
'
diff --git a/owrx/dsp.py b/owrx/dsp.py
index 15a431fc..f5d92356 100644
--- a/owrx/dsp.py
+++ b/owrx/dsp.py
@@ -617,10 +617,10 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
from csdr.chain.digiham import PocsagDemodulator
return PocsagDemodulator()
elif mod == "page":
- from csdr.chain.multimon import PageDemodulator
+ from csdr.chain.toolbox import PageDemodulator
return PageDemodulator()
elif mod == "selcall":
- from csdr.chain.multimon import SelCallDemodulator
+ from csdr.chain.toolbox import SelCallDemodulator
return SelCallDemodulator()
elif mod == "bpsk31":
from csdr.chain.digimodes import PskDemodulator
@@ -644,8 +644,8 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
from csdr.chain.digimodes import FaxDemodulator
return FaxDemodulator()
elif mod == "ism":
- from csdr.chain.rtl433 import Rtl433Demodulator
- return Rtl433Demodulator()
+ from csdr.chain.toolbox import IsmDemodulator
+ return IsmDemodulator()
def setSecondaryDemodulator(self, mod):
demodulator = self._getSecondaryDemodulator(mod)
diff --git a/owrx/modes.py b/owrx/modes.py
index aac3793f..ea26f120 100644
--- a/owrx/modes.py
+++ b/owrx/modes.py
@@ -156,7 +156,7 @@ class Modes(object):
# ),
DigitalMode(
"page",
- "Paging",
+ "Page",
underlying=["nfm"],
bandpass=Bandpass(-6000, 6000),
requirements=["page"],
diff --git a/owrx/rtl433.py b/owrx/rtl433.py
deleted file mode 100644
index 7de643e2..00000000
--- a/owrx/rtl433.py
+++ /dev/null
@@ -1,172 +0,0 @@
-from owrx.storage import Storage
-from csdr.module import ThreadModule
-from pycsdr.types import Format
-from datetime import datetime
-import pickle
-import os
-import re
-import json
-
-import logging
-
-logger = logging.getLogger(__name__)
-
-class Rtl433Parser(ThreadModule):
- def __init__(self, filePrefix: str = "ISM", service: bool = False):
- self.service = service
- self.frequency = 0
- self.data = bytearray(b'')
- self.filePfx = filePrefix
- self.file = None
- self.maxLines = 10000
- self.cntLines = 0
- self.colorBuf = {}
- # Use these colors to mark devices by ID
- self.colors = [
- "#FFFFFF", "#999999", "#FF9999", "#FFCC99", "#FFFF99", "#CCFF99",
- "#99FF99", "#99FFCC", "#99FFFF", "#99CCFF", "#9999FF", "#CC99FF",
- "#FF99FF", "#FF99CC",
- ]
- super().__init__()
-
- def __del__(self):
- # Close currently open file, if any
- self.closeFile()
-
- def closeFile(self):
- if self.file is not None:
- try:
- logger.debug("Closing log file '%s'." % self.fileName)
- self.file.close()
- self.file = None
- # Delete excessive files from storage
- logger.debug("Performing storage cleanup...")
- Storage().cleanStoredFiles()
-
- except Exception as exptn:
- logger.debug("Exception closing file: %s" % str(exptn))
- self.file = None
-
- def newFile(self, fileName):
- self.closeFile()
- try:
- self.fileName = Storage().getFilePath(fileName + ".txt")
- logger.debug("Opening log file '%s'..." % self.fileName)
- self.file = open(self.fileName, "wb")
- self.cntLines = 0
-
- except Exception as exptn:
- logger.debug("Exception opening file: %s" % str(exptn))
- self.file = None
-
- def writeFile(self, data):
- # If no file open, create and open a new file
- if self.file is None:
- self.newFile(Storage().makeFileName(self.filePfx+"-{0}", self.frequency))
- # If file open now...
- if self.file is not None:
- # Write new line into the file
- try:
- self.file.write(data)
- except Exception:
- pass
- # No more than maxLines per file
- self.cntLines = self.cntLines + 1
- if self.cntLines >= self.maxLines:
- self.closeFile()
-
- def getInputFormat(self) -> Format:
- return Format.CHAR
-
- def getOutputFormat(self) -> Format:
- return Format.CHAR
-
- def setDialFrequency(self, frequency: int) -> None:
- self.frequency = frequency
-
- def myName(self):
- return "%s%s" % (
- "Service" if self.service else "Client",
- " at %dkHz" % (self.frequency // 1000) if self.frequency>0 else ""
- )
-
- def getColor(self, id: str) -> str:
- if id in self.colorBuf:
- # Sort entries in order of freshness
- color = self.colorBuf.pop(id)
- elif len(self.colorBuf) < len(self.colors):
- # Assign each initial entry color based on its order
- color = self.colors[len(self.colorBuf)]
- else:
- # If we run out of colors, reuse the oldest entry
- color = self.colorBuf.pop(next(iter(self.colorBuf)))
- # Done
- self.colorBuf[id] = color
- return color
-
- def parse(self, msg: str):
- # Expect JSON data in text form
- out = json.loads(msg)
- out.update({
- "mode": "ISM",
- "color": self.getColor(out["id"])
- })
- return out
-
- def run(self):
- logger.debug("%s starting..." % self.myName())
- # Run while there is input data
- while self.doRun:
- # Read input data
- inp = self.reader.read()
- # Terminate if no input data
- if inp is None:
- logger.debug("%s exiting..." % self.myName())
- self.doRun = False
- break
- # Add read data to the buffer
- self.data = self.data + inp.tobytes()
- # Process buffer contents
- out = self.process()
- # Keep processing while there is input to parse
- while out is not None:
- if len(out)>0:
- if isinstance(out, bytes):
- self.writer.write(out)
- elif isinstance(out, str):
- self.writer.write(bytes(out, 'utf-8'))
- else:
- self.writer.write(pickle.dumps(out))
- out = self.process()
-
- def process(self):
- # No result yet
- out = None
-
- # Search for end-of-line
- eol = self.data.find(b'\n')
-
- # If found end-of-line...
- if eol>=0:
- try:
- msg = self.data[0:eol].decode(encoding="utf-8", errors="replace")
- logger.debug("%s: %s" % (self.myName(), msg))
- # If running as a service...
- if self.service:
- # Write message into open log file, including end-of-line
- self.writeFile(self.data[0:eol+1])
- # Empty result
- out = {}
- else:
- # Let parse() function do its thing
- out = self.parse(msg)
-
- except Exception as exptn:
- logger.debug("%s: Exception parsing: %s" % (self.myName(), str(exptn)))
-
- # Remove parsed message from input, including end-of-line
- del self.data[0:eol+1]
-
- # Return parsed result or None if no result yet
- return out
-
diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py
index 8d188b14..204e3fb2 100644
--- a/owrx/service/__init__.py
+++ b/owrx/service/__init__.py
@@ -319,11 +319,11 @@ class ServiceHandler(SdrSourceEventClient):
from csdr.chain.digimodes import FaxDemodulator
return FaxDemodulator(service=True)
elif mod == "page":
- from csdr.chain.multimon import PageDemodulator
+ from csdr.chain.toolbox import PageDemodulator
return PageDemodulator(service=True)
elif mod == "ism":
- from csdr.chain.rtl433 import Rtl433Demodulator
- return Rtl433Demodulator(service=True)
+ from csdr.chain.toolbox import IsmDemodulator
+ return IsmDemodulator(service=True)
raise ValueError("unsupported service modulation: {}".format(mod))
diff --git a/owrx/multimon.py b/owrx/toolbox.py
similarity index 91%
rename from owrx/multimon.py
rename to owrx/toolbox.py
index c9c34707..b06474e3 100644
--- a/owrx/multimon.py
+++ b/owrx/toolbox.py
@@ -6,13 +6,21 @@ from datetime import datetime
import pickle
import os
import re
+import json
import logging
logger = logging.getLogger(__name__)
-class MultimonParser(ThreadModule):
- def __init__(self, filePrefix: str = "MON", service: bool = False):
+
+class TextParser(ThreadModule):
+ def __init__(self, filePrefix: str = "LOG", service: bool = False):
+ # Use these colors to label messages by address
+ self.colors = [
+ "#FFFFFF", "#999999", "#FF9999", "#FFCC99", "#FFFF99", "#CCFF99",
+ "#99FF99", "#99FFCC", "#99FFFF", "#99CCFF", "#9999FF", "#CC99FF",
+ "#FF99FF", "#FF99CC",
+ ]
self.service = service
self.frequency = 0
self.data = bytearray(b'')
@@ -20,6 +28,7 @@ class MultimonParser(ThreadModule):
self.file = None
self.maxLines = 10000
self.cntLines = 0
+ self.colorBuf = {}
super().__init__()
def __del__(self):
@@ -77,12 +86,29 @@ class MultimonParser(ThreadModule):
def setDialFrequency(self, frequency: int) -> None:
self.frequency = frequency
+ # Compose name of this decoder, made of client/service and frequency
def myName(self):
return "%s%s" % (
"Service" if self.service else "Client",
" at %dkHz" % (self.frequency // 1000) if self.frequency>0 else ""
)
+ # Get a unique color for a given ID, reusing colors as we go
+ def getColor(self, id: str) -> str:
+ if id in self.colorBuf:
+ # Sort entries in order of freshness
+ color = self.colorBuf.pop(id)
+ elif len(self.colorBuf) < len(self.colors):
+ # Assign each initial entry color based on its order
+ color = self.colors[len(self.colorBuf)]
+ else:
+ # If we run out of colors, reuse the oldest entry
+ color = self.colorBuf.pop(next(iter(self.colorBuf)))
+ # Done
+ self.colorBuf[id] = color
+ return color
+
+ # DERIVED CLASSES SHOULD IMPLEMENT THIS FUNCTION!
def parse(self, msg: str):
# By default, do not parse, just return the string
return msg
@@ -145,17 +171,26 @@ class MultimonParser(ThreadModule):
return out
-class PageParser(MultimonParser):
+class IsmParser(TextParser):
+ def __init__(self, service: bool = False):
+ super().__init__(filePrefix="ISM", service=service)
+
+ def parse(self, msg: str):
+ # Expect JSON data in text form
+ out = json.loads(msg)
+ # Add mode name and a color to identify the sender
+ out.update({
+ "mode": "ISM",
+ "color": self.getColor(out["id"])
+ })
+ return out
+
+
+class PageParser(TextParser):
def __init__(self, service: bool = False):
# When true, try filtering out unreadable messages
pm = Config.get()
self.filtering = "paging_filter" in pm and pm["paging_filter"]
- # Use these colors to mark messages by address
- self.colors = [
- "#FFFFFF", "#999999", "#FF9999", "#FFCC99", "#FFFF99", "#CCFF99",
- "#99FF99", "#99FFCC", "#99FFFF", "#99CCFF", "#9999FF", "#CC99FF",
- "#FF99FF", "#FF99CC",
- ]
# POCSAG: Address: Function: (Certainty: )?(Numeric|Alpha|Skyper):
self.rePocsag = re.compile(r"POCSAG(\d+):\s*Address:\s*(\S+)\s+Function:\s*(\S+)(\s+Certainty:.*(\d+))?(\s+(\S+):\s*(.*))?")
# FLEX|NNNN-NN-NN NN:NN:NN|//C/C|NN.NNN|NNNNNNNNN||
@@ -170,8 +205,6 @@ class PageParser(MultimonParser):
self.reSpaces = re.compile(r"[\000-\037\s]+")
# Fragmented messages will be assembled here
self.flexBuf = {}
- # Color assignments will be maintained here
- self.colorBuf = {}
# Construct parent object
super().__init__(filePrefix="PAGE", service=service)
@@ -195,20 +228,6 @@ class PageParser(MultimonParser):
letters = len(msg) - spaces
return (letters > 0) and (letters / (spaces+1) < 40)
- def getColor(self, capcode: str) -> str:
- if capcode in self.colorBuf:
- # Sort entries in order of freshness
- color = self.colorBuf.pop(capcode)
- elif len(self.colorBuf) < len(self.colors):
- # Assign each initial entry color based on its order
- color = self.colors[len(self.colorBuf)]
- else:
- # If we run out of colors, reuse the oldest entry
- color = self.colorBuf.pop(next(iter(self.colorBuf)))
- # Done
- self.colorBuf[capcode] = color
- return color
-
def parsePocsag(self, msg: str):
# No result yet
out = {}
@@ -305,12 +324,13 @@ class PageParser(MultimonParser):
return out
-class SelCallParser(MultimonParser):
+class SelCallParser(TextParser):
def __init__(self, service: bool = False):
self.reSplit = re.compile(r"(ZVEI1|ZVEI2|ZVEI3|DZVEI|PZVEI|DTMF|EEA|EIA|CCIR):\s+")
self.reMatch = re.compile(r"ZVEI1|ZVEI2|ZVEI3|DZVEI|PZVEI|DTMF|EEA|EIA|CCIR")
self.mode = ""
- super().__init__(service)
+ # Construct parent object
+ super().__init__(filePrefix="SELCALL", service=service)
def parse(self, msg: str):
# Parse SELCALL messages