show sdr device log messages in the web configuration
This commit is contained in:
parent
ab40a2934f
commit
13e323cdd2
@ -1,4 +1,5 @@
|
|||||||
**unreleased**
|
**unreleased**
|
||||||
|
- SDR device log messages are now available in the web configuration to simplify troubleshooting
|
||||||
|
|
||||||
**1.2.1**
|
**1.2.1**
|
||||||
- FifiSDR support fixed (pipeline formats now line up correctly)
|
- FifiSDR support fixed (pipeline formats now line up correctly)
|
||||||
|
2
debian/changelog
vendored
2
debian/changelog
vendored
@ -1,4 +1,6 @@
|
|||||||
openwebrx (1.3.0) UNRELEASED; urgency=low
|
openwebrx (1.3.0) UNRELEASED; urgency=low
|
||||||
|
* SDR device log messages are now available in the web configuration to
|
||||||
|
simplify troubleshooting
|
||||||
|
|
||||||
-- Jakob Ketterl <jakob.ketterl@gmx.de> Fri, 30 Sep 2022 16:47:00 +0000
|
-- Jakob Ketterl <jakob.ketterl@gmx.de> Fri, 30 Sep 2022 16:47:00 +0000
|
||||||
|
|
||||||
|
@ -159,4 +159,8 @@ h1 {
|
|||||||
|
|
||||||
.imageupload.is-invalid ~ .invalid-feedback {
|
.imageupload.is-invalid ~ .invalid-feedback {
|
||||||
display: block;
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-log-messages {
|
||||||
|
max-height: 500px;
|
||||||
}
|
}
|
5
htdocs/lib/settings/LogMessages.js
Normal file
5
htdocs/lib/settings/LogMessages.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
$.fn.logMessages = function() {
|
||||||
|
$.each(this, function(){
|
||||||
|
$(this).scrollTop(this.scrollHeight);
|
||||||
|
});
|
||||||
|
};
|
@ -8,4 +8,5 @@ $(function(){
|
|||||||
$('.optional-section').optionalSection();
|
$('.optional-section').optionalSection();
|
||||||
$('#scheduler').schedulerInput();
|
$('#scheduler').schedulerInput();
|
||||||
$('.exponential-input').exponentialInput();
|
$('.exponential-input').exponentialInput();
|
||||||
|
$('.device-log-messages').logMessages();
|
||||||
});
|
});
|
@ -158,6 +158,7 @@ class CompiledAssetsController(GzipMixin, ModificationAwareController):
|
|||||||
"lib/settings/OptionalSection.js",
|
"lib/settings/OptionalSection.js",
|
||||||
"lib/settings/SchedulerInput.js",
|
"lib/settings/SchedulerInput.js",
|
||||||
"lib/settings/ExponentialInput.js",
|
"lib/settings/ExponentialInput.js",
|
||||||
|
"lib/settings/LogMessages.js",
|
||||||
"settings.js",
|
"settings.js",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ from owrx.form.input import TextInput, DropdownInput, Option
|
|||||||
from owrx.form.input.validator import RequiredValidator
|
from owrx.form.input.validator import RequiredValidator
|
||||||
from owrx.property import PropertyLayer
|
from owrx.property import PropertyLayer
|
||||||
from owrx.breadcrumb import BreadcrumbMixin, Breadcrumb, BreadcrumbItem
|
from owrx.breadcrumb import BreadcrumbMixin, Breadcrumb, BreadcrumbItem
|
||||||
|
from owrx.log import HistoryHandler
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@ -279,6 +280,21 @@ class SdrDeviceController(SdrFormControllerWithModal):
|
|||||||
config.store()
|
config.store()
|
||||||
return self.send_redirect("{}settings/sdr".format(self.get_document_root()))
|
return self.send_redirect("{}settings/sdr".format(self.get_document_root()))
|
||||||
|
|
||||||
|
def render_sections(self):
|
||||||
|
handler = HistoryHandler.getHandler("owrx.source.{id}".format(id=self.device_id))
|
||||||
|
return """
|
||||||
|
{sections}
|
||||||
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">Recent device log messages</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<pre class="card-text device-log-messages">{messages}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
""".format(
|
||||||
|
sections=super().render_sections(),
|
||||||
|
messages=handler.getFormattedHistory(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NewSdrDeviceController(SettingsFormController):
|
class NewSdrDeviceController(SettingsFormController):
|
||||||
def __init__(self, handler, request, options):
|
def __init__(self, handler, request, options):
|
||||||
|
52
owrx/log/__init__.py
Normal file
52
owrx/log/__init__.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import threading
|
||||||
|
import os
|
||||||
|
from logging import Logger, Handler, LogRecord, Formatter
|
||||||
|
|
||||||
|
|
||||||
|
class LogPipe(threading.Thread):
|
||||||
|
|
||||||
|
def __init__(self, level: int, logger: Logger, prefix: str = ""):
|
||||||
|
threading.Thread.__init__(self)
|
||||||
|
self.daemon = False
|
||||||
|
self.level = level
|
||||||
|
self.logger = logger
|
||||||
|
self.prefix = prefix
|
||||||
|
self.fdRead, self.fdWrite = os.pipe()
|
||||||
|
self.pipeReader = os.fdopen(self.fdRead)
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
return self.fdWrite
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
for line in iter(self.pipeReader.readline, ''):
|
||||||
|
self.logger.log(self.level, "{}: {}".format(self.prefix, line.strip('\n')))
|
||||||
|
|
||||||
|
self.pipeReader.close()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
os.close(self.fdWrite)
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryHandler(Handler):
|
||||||
|
handlers = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getHandler(name: str):
|
||||||
|
if name not in HistoryHandler.handlers:
|
||||||
|
HistoryHandler.handlers[name] = HistoryHandler()
|
||||||
|
return HistoryHandler.handlers[name]
|
||||||
|
|
||||||
|
def __init__(self, maxRecords: int = 200):
|
||||||
|
super().__init__()
|
||||||
|
self.history = []
|
||||||
|
self.maxRecords = maxRecords
|
||||||
|
self.setFormatter(Formatter(fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
|
||||||
|
|
||||||
|
def emit(self, record: LogRecord) -> None:
|
||||||
|
self.history.append(record)
|
||||||
|
# truncate
|
||||||
|
self.history = self.history[-self.maxRecords:]
|
||||||
|
|
||||||
|
def getFormattedHistory(self) -> str:
|
||||||
|
return "\n".join([self.format(r) for r in self.history])
|
@ -18,6 +18,7 @@ from owrx.form.input.device import GainInput, SchedulerInput, WaterfallLevelsInp
|
|||||||
from owrx.form.input.validator import RequiredValidator
|
from owrx.form.input.validator import RequiredValidator
|
||||||
from owrx.form.section import OptionalSection
|
from owrx.form.section import OptionalSection
|
||||||
from owrx.feature import FeatureDetector
|
from owrx.feature import FeatureDetector
|
||||||
|
from owrx.log import LogPipe, HistoryHandler
|
||||||
from typing import List
|
from typing import List
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
@ -114,6 +115,10 @@ class SdrSource(ABC):
|
|||||||
self.commandMapper = None
|
self.commandMapper = None
|
||||||
self.tcpSource = None
|
self.tcpSource = None
|
||||||
self.buffer = None
|
self.buffer = None
|
||||||
|
self.logger = logger.getChild(id) if id is not None else logger
|
||||||
|
self.logger.addHandler(HistoryHandler.getHandler(self.logger.name))
|
||||||
|
self.stdoutPipe = None
|
||||||
|
self.stderrPipe = None
|
||||||
|
|
||||||
self.props = PropertyStack()
|
self.props = PropertyStack()
|
||||||
|
|
||||||
@ -185,17 +190,17 @@ class SdrSource(ABC):
|
|||||||
for id, p in self.props["profiles"].items():
|
for id, p in self.props["profiles"].items():
|
||||||
props.replaceLayer(0, p)
|
props.replaceLayer(0, p)
|
||||||
if "center_freq" not in props:
|
if "center_freq" not in props:
|
||||||
logger.warning('Profile "%s" does not specify a center_freq', id)
|
self.logger.warning('Profile "%s" does not specify a center_freq', id)
|
||||||
continue
|
continue
|
||||||
if "samp_rate" not in props:
|
if "samp_rate" not in props:
|
||||||
logger.warning('Profile "%s" does not specify a samp_rate', id)
|
self.logger.warning('Profile "%s" does not specify a samp_rate', id)
|
||||||
continue
|
continue
|
||||||
if "start_freq" in props:
|
if "start_freq" in props:
|
||||||
start_freq = props["start_freq"]
|
start_freq = props["start_freq"]
|
||||||
srh = props["samp_rate"] / 2
|
srh = props["samp_rate"] / 2
|
||||||
center_freq = props["center_freq"]
|
center_freq = props["center_freq"]
|
||||||
if start_freq < center_freq - srh or start_freq > center_freq + srh:
|
if start_freq < center_freq - srh or start_freq > center_freq + srh:
|
||||||
logger.warning('start_freq for profile "%s" is out of range', id)
|
self.logger.warning('start_freq for profile "%s" is out of range', id)
|
||||||
|
|
||||||
def isAlwaysOn(self):
|
def isAlwaysOn(self):
|
||||||
return "always-on" in self.props and self.props["always-on"]
|
return "always-on" in self.props and self.props["always-on"]
|
||||||
@ -225,11 +230,11 @@ class SdrSource(ABC):
|
|||||||
return [self.getCommandMapper().map(self.getCommandValues())]
|
return [self.getCommandMapper().map(self.getCommandValues())]
|
||||||
|
|
||||||
def activateProfile(self, profile_id):
|
def activateProfile(self, profile_id):
|
||||||
logger.debug("activating profile {0} for {1}".format(profile_id, self.getId()))
|
self.logger.debug("activating profile {0} for {1}".format(profile_id, self.getId()))
|
||||||
try:
|
try:
|
||||||
self.profileCarousel.switch(profile_id)
|
self.profileCarousel.switch(profile_id)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.getId())
|
self.logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.getId())
|
||||||
|
|
||||||
def getId(self):
|
def getId(self):
|
||||||
return self.id
|
return self.id
|
||||||
@ -283,23 +288,37 @@ class SdrSource(ABC):
|
|||||||
try:
|
try:
|
||||||
self.preStart()
|
self.preStart()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Exception during preStart()")
|
self.logger.exception("Exception during preStart()")
|
||||||
|
|
||||||
cmd = self.getCommand()
|
cmd = self.getCommand()
|
||||||
cmd = [c for c in cmd if c is not None]
|
cmd = [c for c in cmd if c is not None]
|
||||||
|
|
||||||
|
self.stdoutPipe = LogPipe(logging.INFO, self.logger, "STDOUT")
|
||||||
|
self.stderrPipe = LogPipe(logging.WARNING, self.logger, "STDERR")
|
||||||
|
|
||||||
# don't use shell mode for commands without piping
|
# don't use shell mode for commands without piping
|
||||||
if len(cmd) > 1:
|
if len(cmd) > 1:
|
||||||
# multiple commands with pipes
|
# multiple commands with pipes
|
||||||
cmd = "|".join(cmd)
|
cmd = "|".join(cmd)
|
||||||
self.process = subprocess.Popen(cmd, shell=True, start_new_session=True)
|
self.process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
shell=True,
|
||||||
|
start_new_session=True,
|
||||||
|
stdout=self.stdoutPipe,
|
||||||
|
stderr=self.stderrPipe
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# single command
|
# single command
|
||||||
cmd = cmd[0]
|
cmd = cmd[0]
|
||||||
# start_new_session can go as soon as there's no piped commands left
|
# start_new_session can go as soon as there's no piped commands left
|
||||||
# the os.killpg call must be replaced with something more reasonable at the same time
|
# the os.killpg call must be replaced with something more reasonable at the same time
|
||||||
self.process = subprocess.Popen(shlex.split(cmd), start_new_session=True)
|
self.process = subprocess.Popen(
|
||||||
logger.info("Started sdr source: " + cmd)
|
shlex.split(cmd),
|
||||||
|
start_new_session=True,
|
||||||
|
stdout=self.stdoutPipe,
|
||||||
|
stderr=self.stderrPipe
|
||||||
|
)
|
||||||
|
self.logger.info("Started sdr source: " + cmd)
|
||||||
|
|
||||||
available = False
|
available = False
|
||||||
failed = False
|
failed = False
|
||||||
@ -307,9 +326,13 @@ class SdrSource(ABC):
|
|||||||
def wait_for_process_to_end():
|
def wait_for_process_to_end():
|
||||||
nonlocal failed
|
nonlocal failed
|
||||||
rc = self.process.wait()
|
rc = self.process.wait()
|
||||||
logger.debug("shut down with RC={0}".format(rc))
|
self.logger.debug("shut down with RC={0}".format(rc))
|
||||||
self.process = None
|
self.process = None
|
||||||
self.monitor = None
|
self.monitor = None
|
||||||
|
self.stdoutPipe.close()
|
||||||
|
self.stdoutPipe = None
|
||||||
|
self.stderrPipe.close()
|
||||||
|
self.stderrPipe = None
|
||||||
if self.getState() is SdrSourceState.RUNNING:
|
if self.getState() is SdrSourceState.RUNNING:
|
||||||
self.fail()
|
self.fail()
|
||||||
else:
|
else:
|
||||||
@ -340,7 +363,7 @@ class SdrSource(ABC):
|
|||||||
try:
|
try:
|
||||||
self.postStart()
|
self.postStart()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Exception during postStart()")
|
self.logger.exception("Exception during postStart()")
|
||||||
failed = True
|
failed = True
|
||||||
|
|
||||||
if failed:
|
if failed:
|
||||||
@ -374,7 +397,7 @@ class SdrSource(ABC):
|
|||||||
self.monitor.join(10)
|
self.monitor.join(10)
|
||||||
# if the monitor is still running, the process still hasn't ended, so kill it
|
# if the monitor is still running, the process still hasn't ended, so kill it
|
||||||
if self.monitor:
|
if self.monitor:
|
||||||
logger.warning("source has not shut down normally within 10 seconds, sending SIGKILL")
|
self.logger.warning("source has not shut down normally within 10 seconds, sending SIGKILL")
|
||||||
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
|
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
|
||||||
except ProcessLookupError:
|
except ProcessLookupError:
|
||||||
# been killed by something else, ignore
|
# been killed by something else, ignore
|
||||||
|
@ -6,10 +6,6 @@ from owrx.command import Flag, Option
|
|||||||
from typing import List
|
from typing import List
|
||||||
from owrx.form.input import Input, NumberInput, CheckboxInput
|
from owrx.form.input import Input, NumberInput, CheckboxInput
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectorSource(SdrSource):
|
class ConnectorSource(SdrSource):
|
||||||
def __init__(self, id, props):
|
def __init__(self, id, props):
|
||||||
@ -40,7 +36,7 @@ class ConnectorSource(SdrSource):
|
|||||||
for prop, value in changes.items():
|
for prop, value in changes.items():
|
||||||
if value is PropertyDeleted:
|
if value is PropertyDeleted:
|
||||||
value = None
|
value = None
|
||||||
logger.debug("sending property change over control socket: {0} changed to {1}".format(prop, value))
|
self.logger.debug("sending property change over control socket: {0} changed to {1}".format(prop, value))
|
||||||
self.controlSocket.sendall("{prop}:{value}\n".format(prop=prop, value=value).encode())
|
self.controlSocket.sendall("{prop}:{value}\n".format(prop=prop, value=value).encode())
|
||||||
|
|
||||||
def onPropertyChange(self, changes):
|
def onPropertyChange(self, changes):
|
||||||
@ -56,7 +52,7 @@ class ConnectorSource(SdrSource):
|
|||||||
self.sendControlMessage(changes)
|
self.sendControlMessage(changes)
|
||||||
|
|
||||||
def postStart(self):
|
def postStart(self):
|
||||||
logger.debug("opening control socket...")
|
self.logger.debug("opening control socket...")
|
||||||
self.controlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
self.controlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
self.controlSocket.connect(("localhost", self.controlPort))
|
self.controlSocket.connect(("localhost", self.controlPort))
|
||||||
|
|
||||||
|
@ -5,10 +5,6 @@ from typing import Optional
|
|||||||
from pycsdr.modules import Buffer
|
from pycsdr.modules import Buffer
|
||||||
from pycsdr.types import Format
|
from pycsdr.types import Format
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DirectSource(SdrSource, metaclass=ABCMeta):
|
class DirectSource(SdrSource, metaclass=ABCMeta):
|
||||||
def __init__(self, id, props):
|
def __init__(self, id, props):
|
||||||
@ -16,7 +12,7 @@ class DirectSource(SdrSource, metaclass=ABCMeta):
|
|||||||
super().__init__(id, props)
|
super().__init__(id, props)
|
||||||
|
|
||||||
def onPropertyChange(self, changes):
|
def onPropertyChange(self, changes):
|
||||||
logger.debug("restarting sdr source due to property changes: {0}".format(changes))
|
self.logger.debug("restarting sdr source due to property changes: {0}".format(changes))
|
||||||
self.stop()
|
self.stop()
|
||||||
self.sleepOnRestart()
|
self.sleepOnRestart()
|
||||||
self.start()
|
self.start()
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
from owrx.command import Option
|
from owrx.command import Option
|
||||||
from owrx.source.direct import DirectSource, DirectSourceDeviceDescription
|
from owrx.source.direct import DirectSource, DirectSourceDeviceDescription
|
||||||
|
from owrx.log import LogPipe
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
from csdr.chain import Chain
|
from csdr.chain import Chain
|
||||||
from pycsdr.modules import Convert, Gain
|
from pycsdr.modules import Convert, Gain
|
||||||
from pycsdr.types import Format
|
from pycsdr.types import Format
|
||||||
from typing import List
|
from typing import List
|
||||||
from owrx.form.input import Input, TextInput
|
from owrx.form.input import Input, TextInput
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FifiSdrSource(DirectSource):
|
class FifiSdrSource(DirectSource):
|
||||||
def getCommandMapper(self):
|
def getCommandMapper(self):
|
||||||
@ -29,11 +27,19 @@ class FifiSdrSource(DirectSource):
|
|||||||
return Chain([Convert(Format.COMPLEX_SHORT, Format.COMPLEX_FLOAT), Gain(Format.COMPLEX_FLOAT, 5.0)])
|
return Chain([Convert(Format.COMPLEX_SHORT, Format.COMPLEX_FLOAT), Gain(Format.COMPLEX_FLOAT, 5.0)])
|
||||||
|
|
||||||
def sendRockProgFrequency(self, frequency):
|
def sendRockProgFrequency(self, frequency):
|
||||||
process = Popen(["rockprog", "--vco", "-w", "--freq={}".format(frequency / 1e6)])
|
stdoutPipe = LogPipe(logging.DEBUG, self.logger, "STDOUT")
|
||||||
|
stderrPipe = LogPipe(logging.DEBUG, self.logger, "STDERR")
|
||||||
|
process = Popen(
|
||||||
|
["rockprog", "--vco", "-w", "--freq={}".format(frequency / 1e6)],
|
||||||
|
stdout=stdoutPipe,
|
||||||
|
stderr=stderrPipe
|
||||||
|
)
|
||||||
process.communicate()
|
process.communicate()
|
||||||
rc = process.wait()
|
rc = process.wait()
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
logger.warning("rockprog failed to set frequency; rc=%i", rc)
|
self.logger.warning("rockprog failed to set frequency; rc=%i", rc)
|
||||||
|
stdoutPipe.close()
|
||||||
|
stderrPipe.close()
|
||||||
|
|
||||||
def preStart(self):
|
def preStart(self):
|
||||||
values = self.getCommandValues()
|
values = self.getCommandValues()
|
||||||
|
@ -3,14 +3,10 @@ from pycsdr.modules import Buffer, FirDecimate, Shift
|
|||||||
from pycsdr.types import Format
|
from pycsdr.types import Format
|
||||||
from csdr.chain import Chain
|
from csdr.chain import Chain
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Resampler(SdrSource):
|
class Resampler(SdrSource):
|
||||||
def onPropertyChange(self, changes):
|
def onPropertyChange(self, changes):
|
||||||
logger.warning("Resampler is unable to handle property changes: {0}".format(changes))
|
self.logger.warning("Resampler is unable to handle property changes: {0}".format(changes))
|
||||||
|
|
||||||
def __init__(self, props, sdr):
|
def __init__(self, props, sdr):
|
||||||
sdrProps = sdr.getProps()
|
sdrProps = sdr.getProps()
|
||||||
@ -41,7 +37,7 @@ class Resampler(SdrSource):
|
|||||||
super().stop()
|
super().stop()
|
||||||
|
|
||||||
def activateProfile(self, profile_id=None):
|
def activateProfile(self, profile_id=None):
|
||||||
logger.warning("Resampler does not support setting profiles")
|
self.logger.warning("Resampler does not support setting profiles")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def validateProfiles(self):
|
def validateProfiles(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user