From 12be0825232fbe4c57095a65b9317326ba6583be Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 17 Jan 2020 22:46:01 +0100 Subject: [PATCH 001/475] refactor service / schedule code in preparation for alternate schedulers --- owrx/{service.py => service/__init__.py} | 148 +------------------ owrx/service/schedule.py | 174 +++++++++++++++++++++++ setup.py | 2 +- 3 files changed, 182 insertions(+), 142 deletions(-) rename owrx/{service.py => service/__init__.py} (67%) create mode 100644 owrx/service/schedule.py diff --git a/owrx/service.py b/owrx/service/__init__.py similarity index 67% rename from owrx/service.py rename to owrx/service/__init__.py index 5936f8f..3e1a337 100644 --- a/owrx/service.py +++ b/owrx/service/__init__.py @@ -1,5 +1,4 @@ import threading -from datetime import datetime, timezone, timedelta from owrx.source import SdrSource from owrx.sdr import SdrService from owrx.bands import Bandplan @@ -9,16 +8,19 @@ from owrx.aprs import AprsParser from owrx.config import PropertyManager from owrx.source.resampler import Resampler from owrx.feature import FeatureDetector +from abc import ABCMeta, abstractmethod +from .schedule import ServiceScheduler import logging logger = logging.getLogger(__name__) -class ServiceOutput(output): +class ServiceOutput(output, metaclass=ABCMeta): def __init__(self, frequency): self.frequency = frequency + @abstractmethod def getParser(self): # abstract method; implement in subclasses pass @@ -46,134 +48,6 @@ class AprsServiceOutput(ServiceOutput): return t == "packet_demod" -class ScheduleEntry(object): - def __init__(self, startTime, endTime, profile): - self.startTime = startTime - self.endTime = endTime - self.profile = profile - - def isCurrent(self, time): - if self.startTime < self.endTime: - return self.startTime <= time < self.endTime - else: - return self.startTime <= time or time < self.endTime - - def getProfile(self): - return self.profile - - def getScheduledEnd(self): - now = datetime.utcnow() - end = now.combine(date=now.date(), time=self.endTime) - while end < now: - end += timedelta(days=1) - return end - - def getNextActivation(self): - now = datetime.utcnow() - start = now.combine(date=now.date(), time=self.startTime) - while start < now: - start += timedelta(days=1) - return start - - -class Schedule(object): - @staticmethod - def parse(scheduleDict): - entries = [] - for time, profile in scheduleDict.items(): - if len(time) != 9: - logger.warning("invalid schedule spec: %s", time) - continue - - startTime = datetime.strptime(time[0:4], "%H%M").replace(tzinfo=timezone.utc).time() - endTime = datetime.strptime(time[5:9], "%H%M").replace(tzinfo=timezone.utc).time() - entries.append(ScheduleEntry(startTime, endTime, profile)) - return Schedule(entries) - - def __init__(self, entries): - self.entries = entries - - def getCurrentEntry(self): - current = [p for p in self.entries if p.isCurrent(datetime.utcnow().time())] - if current: - return current[0] - return None - - def getNextEntry(self): - s = sorted(self.entries, key=lambda e: e.getNextActivation()) - if s: - return s[0] - return None - - -class ServiceScheduler(object): - def __init__(self, source, schedule): - self.source = source - self.selectionTimer = None - self.schedule = Schedule.parse(schedule) - self.source.addClient(self) - self.source.getProps().collect("center_freq", "samp_rate").wire(self.onFrequencyChange) - self.scheduleSelection() - - def shutdown(self): - self.cancelTimer() - self.source.removeClient(self) - - def scheduleSelection(self, time=None): - if self.source.getState() == SdrSource.STATE_FAILED: - return - seconds = 10 - if time is not None: - delta = time - datetime.utcnow() - seconds = delta.total_seconds() - self.cancelTimer() - self.selectionTimer = threading.Timer(seconds, self.selectProfile) - self.selectionTimer.start() - - def cancelTimer(self): - if self.selectionTimer: - self.selectionTimer.cancel() - - def getClientClass(self): - return SdrSource.CLIENT_BACKGROUND - - def onStateChange(self, state): - if state == SdrSource.STATE_STOPPING: - self.scheduleSelection() - elif state == SdrSource.STATE_FAILED: - self.cancelTimer() - - def onBusyStateChange(self, state): - if state == SdrSource.BUSYSTATE_IDLE: - self.scheduleSelection() - - def onFrequencyChange(self, name, value): - self.scheduleSelection() - - def selectProfile(self): - if self.source.hasClients(SdrSource.CLIENT_USER): - logger.debug("source has active users; not touching") - return - logger.debug("source seems to be idle, selecting profile for background services") - entry = self.schedule.getCurrentEntry() - - if entry is None: - logger.debug("schedule did not return a profile. checking next entry...") - nextEntry = self.schedule.getNextEntry() - if nextEntry is not None: - self.scheduleSelection(nextEntry.getNextActivation()) - return - - logger.debug("scheduling end for current profile: %s", entry.getScheduledEnd()) - self.scheduleSelection(entry.getScheduledEnd()) - - try: - self.source.activateProfile(entry.getProfile()) - self.source.start() - except KeyError: - pass - - class ServiceHandler(object): def __init__(self, source): self.lock = threading.Lock() @@ -186,8 +60,8 @@ class ServiceHandler(object): if self.source.isAvailable(): self.scheduleServiceStartup() self.scheduler = None - if "schedule" in props: - self.scheduler = ServiceScheduler(self.source, props["schedule"]) + if "schedule" in props or "scheduler" in props: + self.scheduler = ServiceScheduler(self.source) def getClientClass(self): return SdrSource.CLIENT_INACTIVE @@ -390,7 +264,7 @@ class Services(object): return for source in SdrService.getSources().values(): props = source.getProps() - if "services" not in props or props["services"] != False: + if "services" not in props or props["services"] is not False: Services.handlers.append(ServiceHandler(source)) @staticmethod @@ -398,11 +272,3 @@ class Services(object): for handler in Services.handlers: handler.shutdown() Services.handlers = [] - - -class Service(object): - pass - - -class WsjtService(Service): - pass diff --git a/owrx/service/schedule.py b/owrx/service/schedule.py new file mode 100644 index 0000000..282133c --- /dev/null +++ b/owrx/service/schedule.py @@ -0,0 +1,174 @@ +from datetime import datetime, timezone, timedelta +from owrx.source import SdrSource +import threading +from abc import ABC, ABCMeta, abstractmethod + +import logging + +logger = logging.getLogger(__name__) + + +class ScheduleEntry(object): + def __init__(self, startTime, endTime, profile): + self.startTime = startTime + self.endTime = endTime + self.profile = profile + + def isCurrent(self, time): + if self.startTime < self.endTime: + return self.startTime <= time < self.endTime + else: + return self.startTime <= time or time < self.endTime + + def getProfile(self): + return self.profile + + def getScheduledEnd(self): + now = datetime.utcnow() + end = now.combine(date=now.date(), time=self.endTime) + while end < now: + end += timedelta(days=1) + return end + + def getNextActivation(self): + now = datetime.utcnow() + start = now.combine(date=now.date(), time=self.startTime) + while start < now: + start += timedelta(days=1) + return start + + +class Schedule(ABC): + @staticmethod + def parse(props): + # downwards compatibility + if "schedule" in props: + return StaticSchedule(props["schedule"]) + elif "scheduler" in props: + sc = props["scheduler"] + t = sc["type"] if "type" in sc else "static" + if t == "static": + return StaticSchedule(sc["schedule"]) + elif t == "sunlight": + return SunlightSchedule(sc["schedule"]) + else: + logger.warning("Invalid scheduler type: %s", t) + + @abstractmethod + def getCurrentEntry(self): + pass + + @abstractmethod + def getNextEntry(self): + pass + + +class TimerangeSchedule(Schedule, metaclass=ABCMeta): + @abstractmethod + def getEntries(self): + pass + + def getCurrentEntry(self): + current = [p for p in self.getEntries() if p.isCurrent(datetime.utcnow().time())] + if current: + return current[0] + return None + + def getNextEntry(self): + s = sorted(self.getEntries(), key=lambda e: e.getNextActivation()) + if s: + return s[0] + return None + + +class StaticSchedule(TimerangeSchedule): + def __init__(self, scheduleDict): + self.entries = [] + for time, profile in scheduleDict.items(): + if len(time) != 9: + logger.warning("invalid schedule spec: %s", time) + continue + + startTime = datetime.strptime(time[0:4], "%H%M").replace(tzinfo=timezone.utc).time() + endTime = datetime.strptime(time[5:9], "%H%M").replace(tzinfo=timezone.utc).time() + self.entries.append(ScheduleEntry(startTime, endTime, profile)) + + def getEntries(self): + return self.entries + + +class SunlightSchedule(TimerangeSchedule): + def __init__(self, scheduleDict): + self.schedule = scheduleDict + + def getEntries(self): + return [] + + +class ServiceScheduler(object): + def __init__(self, source): + self.source = source + self.selectionTimer = None + self.source.addClient(self) + props = self.source.getProps() + self.schedule = Schedule.parse(props) + props.collect("center_freq", "samp_rate").wire(self.onFrequencyChange) + self.scheduleSelection() + + def shutdown(self): + self.cancelTimer() + self.source.removeClient(self) + + def scheduleSelection(self, time=None): + if self.source.getState() == SdrSource.STATE_FAILED: + return + seconds = 10 + if time is not None: + delta = time - datetime.utcnow() + seconds = delta.total_seconds() + self.cancelTimer() + self.selectionTimer = threading.Timer(seconds, self.selectProfile) + self.selectionTimer.start() + + def cancelTimer(self): + if self.selectionTimer: + self.selectionTimer.cancel() + + def getClientClass(self): + return SdrSource.CLIENT_BACKGROUND + + def onStateChange(self, state): + if state == SdrSource.STATE_STOPPING: + self.scheduleSelection() + elif state == SdrSource.STATE_FAILED: + self.cancelTimer() + + def onBusyStateChange(self, state): + if state == SdrSource.BUSYSTATE_IDLE: + self.scheduleSelection() + + def onFrequencyChange(self, name, value): + self.scheduleSelection() + + def selectProfile(self): + if self.source.hasClients(SdrSource.CLIENT_USER): + logger.debug("source has active users; not touching") + return + logger.debug("source seems to be idle, selecting profile for background services") + entry = self.schedule.getCurrentEntry() + + if entry is None: + logger.debug("schedule did not return a profile. checking next entry...") + nextEntry = self.schedule.getNextEntry() + if nextEntry is not None: + self.scheduleSelection(nextEntry.getNextActivation()) + return + + logger.debug("scheduling end for current profile: %s", entry.getScheduledEnd()) + self.scheduleSelection(entry.getScheduledEnd()) + + try: + self.source.activateProfile(entry.getProfile()) + self.source.start() + except KeyError: + pass diff --git a/setup.py b/setup.py index 03c0a30..dd4fe70 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ except ImportError: setup( name="OpenWebRX", version=str(strictversion), - packages=find_namespace_packages(include=["owrx", "owrx.source", "csdr", "htdocs"]), + packages=find_namespace_packages(include=["owrx", "owrx.source", "owrx.service", "csdr", "htdocs"]), package_data={"htdocs": [f[len("htdocs/") :] for f in glob("htdocs/**/*", recursive=True)]}, entry_points={"console_scripts": ["openwebrx=owrx.__main__:main"]}, # use the github page for now From f826002ea82a0e3e9fe024730f57b246524b02ec Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 18 Jan 2020 00:43:37 +0100 Subject: [PATCH 002/475] enable solar calculations --- owrx/service/schedule.py | 52 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/owrx/service/schedule.py b/owrx/service/schedule.py index 282133c..1abb2b4 100644 --- a/owrx/service/schedule.py +++ b/owrx/service/schedule.py @@ -1,6 +1,8 @@ from datetime import datetime, timezone, timedelta from owrx.source import SdrSource +from owrx.config import PropertyManager import threading +import math from abc import ABC, ABCMeta, abstractmethod import logging @@ -101,8 +103,55 @@ class SunlightSchedule(TimerangeSchedule): def __init__(self, scheduleDict): self.schedule = scheduleDict + def getSunTimes(self, date): + pm = PropertyManager.getSharedInstance() + lat, lng = pm["receiver_gps"] + degtorad = math.pi / 180 + radtodeg = 180 / math.pi + + nDays = date.timetuple().tm_yday #Number of days since 01/01 + + # Longitudinal correction + longCorr = 4 * lng + + B = 2 * math.pi * (nDays - 81) / 365 # I have no idea + + # Equation of Time Correction + EoTCorr = 9.87 * math.sin(2 * B) - 7.53 * math.cos(B) - 1.5 * math.sin(B) + + # Solar correction + solarCorr = longCorr + EoTCorr + + # Solar declination + delta = math.asin(math.sin(23.45 * degtorad) * math.sin(B)) * radtodeg + + sunrise = 12 - math.acos(-math.tan(lat * degtorad) * math.tan(delta * degtorad)) * radtodeg / 15 - solarCorr / 60 + sunset = 12 + math.acos(-math.tan(lat * degtorad) * math.tan(delta * degtorad)) * radtodeg / 15 - solarCorr / 60 + + midnight = datetime.combine(date, datetime.min.time()) + sunrise = midnight + timedelta(hours=sunrise) + sunset = midnight + timedelta(hours=sunset) + + return sunrise, sunset + + def getEntry(self, t, profile): + now = datetime.utcnow() + date = now.date() + if t == "day": + sunrise, sunset = self.getSunTimes(date) + if sunset < now: + sunrise, sunset = self.getSunTimes(date + timedelta(days=1)) + return ScheduleEntry(sunrise.time(), sunset.time(), profile) + elif t == "night": + sunrise, _ = self.getSunTimes(date) + _, sunset = self.getSunTimes(date - timedelta(days=1)) + if sunrise < now: + sunrise, _ = self.getSunTimes(date + timedelta(days=1)) + _, sunset = self.getSunTimes(date) + return ScheduleEntry(sunset.time(), sunrise.time(), profile) + def getEntries(self): - return [] + return [self.getEntry(t, profile) for t, profile in self.schedule.items()] class ServiceScheduler(object): @@ -168,6 +217,7 @@ class ServiceScheduler(object): self.scheduleSelection(entry.getScheduledEnd()) try: + logger.debug("scheduler is activating profile %s", entry.getProfile()) self.source.activateProfile(entry.getProfile()) self.source.start() except KeyError: From b142180f94e03e09b14b45710f3dad89b75577f1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 18 Jan 2020 17:35:33 +0100 Subject: [PATCH 003/475] optimize --- owrx/service/schedule.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/owrx/service/schedule.py b/owrx/service/schedule.py index 1abb2b4..0061d04 100644 --- a/owrx/service/schedule.py +++ b/owrx/service/schedule.py @@ -114,23 +114,25 @@ class SunlightSchedule(TimerangeSchedule): # Longitudinal correction longCorr = 4 * lng - B = 2 * math.pi * (nDays - 81) / 365 # I have no idea + # calibrate for solstice + b = 2 * math.pi * (nDays - 81) / 365 # Equation of Time Correction - EoTCorr = 9.87 * math.sin(2 * B) - 7.53 * math.cos(B) - 1.5 * math.sin(B) + eoTCorr = 9.87 * math.sin(2 * b) - 7.53 * math.cos(b) - 1.5 * math.sin(b) # Solar correction - solarCorr = longCorr + EoTCorr + solarCorr = longCorr + eoTCorr # Solar declination - delta = math.asin(math.sin(23.45 * degtorad) * math.sin(B)) * radtodeg + delta = math.asin(math.sin(23.45 * degtorad) * math.sin(b)) - sunrise = 12 - math.acos(-math.tan(lat * degtorad) * math.tan(delta * degtorad)) * radtodeg / 15 - solarCorr / 60 - sunset = 12 + math.acos(-math.tan(lat * degtorad) * math.tan(delta * degtorad)) * radtodeg / 15 - solarCorr / 60 + sunrise = 12 - math.acos(-math.tan(lat * degtorad) * math.tan(delta)) * radtodeg / 15 - solarCorr / 60 + sunset = 12 + math.acos(-math.tan(lat * degtorad) * math.tan(delta)) * radtodeg / 15 - solarCorr / 60 midnight = datetime.combine(date, datetime.min.time()) sunrise = midnight + timedelta(hours=sunrise) sunset = midnight + timedelta(hours=sunset) + logger.debug("for {date} sunrise: {sunrise} sunset {sunset}".format(date=date, sunrise=sunrise, sunset=sunset)) return sunrise, sunset From 877f0e4c28647e06793d581483eaf5e7a164b951 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 Jan 2020 17:04:14 +0100 Subject: [PATCH 004/475] allow schedule entries with datetime --- owrx/service/schedule.py | 55 ++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/owrx/service/schedule.py b/owrx/service/schedule.py index 0061d04..cbcf975 100644 --- a/owrx/service/schedule.py +++ b/owrx/service/schedule.py @@ -10,21 +10,39 @@ import logging logger = logging.getLogger(__name__) -class ScheduleEntry(object): +class ScheduleEntry(ABC): def __init__(self, startTime, endTime, profile): self.startTime = startTime self.endTime = endTime self.profile = profile - def isCurrent(self, time): + def getProfile(self): + return self.profile + + def __str__(self): + return "{0} - {1}: {2}".format(self.startTime, self.endTime, self.profile) + + @abstractmethod + def isCurrent(self, dt): + pass + + @abstractmethod + def getScheduledEnd(self): + pass + + @abstractmethod + def getNextActivation(self): + pass + + +class TimeScheduleEntry(ScheduleEntry): + def isCurrent(self, dt): + time = dt.time() if self.startTime < self.endTime: return self.startTime <= time < self.endTime else: return self.startTime <= time or time < self.endTime - def getProfile(self): - return self.profile - def getScheduledEnd(self): now = datetime.utcnow() end = now.combine(date=now.date(), time=self.endTime) @@ -40,6 +58,16 @@ class ScheduleEntry(object): return start +class DatetimeScheduleEntry(ScheduleEntry): + def isCurrent(self, dt): + return self.startTime <= dt < self.endTime + + def getScheduledEnd(self): + return self.endTime + + def getNextActivation(self): + return self.startTime + class Schedule(ABC): @staticmethod def parse(props): @@ -51,8 +79,8 @@ class Schedule(ABC): t = sc["type"] if "type" in sc else "static" if t == "static": return StaticSchedule(sc["schedule"]) - elif t == "sunlight": - return SunlightSchedule(sc["schedule"]) + elif t == "daylight": + return DaylightSchedule(sc["schedule"]) else: logger.warning("Invalid scheduler type: %s", t) @@ -71,7 +99,7 @@ class TimerangeSchedule(Schedule, metaclass=ABCMeta): pass def getCurrentEntry(self): - current = [p for p in self.getEntries() if p.isCurrent(datetime.utcnow().time())] + current = [p for p in self.getEntries() if p.isCurrent(datetime.utcnow())] if current: return current[0] return None @@ -93,13 +121,13 @@ class StaticSchedule(TimerangeSchedule): startTime = datetime.strptime(time[0:4], "%H%M").replace(tzinfo=timezone.utc).time() endTime = datetime.strptime(time[5:9], "%H%M").replace(tzinfo=timezone.utc).time() - self.entries.append(ScheduleEntry(startTime, endTime, profile)) + self.entries.append(TimeScheduleEntry(startTime, endTime, profile)) def getEntries(self): return self.entries -class SunlightSchedule(TimerangeSchedule): +class DaylightSchedule(TimerangeSchedule): def __init__(self, scheduleDict): self.schedule = scheduleDict @@ -143,14 +171,14 @@ class SunlightSchedule(TimerangeSchedule): sunrise, sunset = self.getSunTimes(date) if sunset < now: sunrise, sunset = self.getSunTimes(date + timedelta(days=1)) - return ScheduleEntry(sunrise.time(), sunset.time(), profile) + return DatetimeScheduleEntry(sunrise, sunset, profile) elif t == "night": sunrise, _ = self.getSunTimes(date) _, sunset = self.getSunTimes(date - timedelta(days=1)) if sunrise < now: sunrise, _ = self.getSunTimes(date + timedelta(days=1)) _, sunset = self.getSunTimes(date) - return ScheduleEntry(sunset.time(), sunrise.time(), profile) + return DatetimeScheduleEntry(sunset, sunrise, profile) def getEntries(self): return [self.getEntry(t, profile) for t, profile in self.schedule.items()] @@ -215,11 +243,10 @@ class ServiceScheduler(object): self.scheduleSelection(nextEntry.getNextActivation()) return - logger.debug("scheduling end for current profile: %s", entry.getScheduledEnd()) + logger.debug("selected profile %s until %s", entry.getProfile(), entry.getScheduledEnd()) self.scheduleSelection(entry.getScheduledEnd()) try: - logger.debug("scheduler is activating profile %s", entry.getProfile()) self.source.activateProfile(entry.getProfile()) self.source.start() except KeyError: From 1f68ecd9f4ef19ef0779d51802273d800ea99600 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 Jan 2020 18:34:37 +0100 Subject: [PATCH 005/475] add greyline calculation --- owrx/service/schedule.py | 45 ++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/owrx/service/schedule.py b/owrx/service/schedule.py index cbcf975..d5d7388 100644 --- a/owrx/service/schedule.py +++ b/owrx/service/schedule.py @@ -128,6 +128,8 @@ class StaticSchedule(TimerangeSchedule): class DaylightSchedule(TimerangeSchedule): + greyLineTime = timedelta(hours=1) + def __init__(self, scheduleDict): self.schedule = scheduleDict @@ -137,13 +139,14 @@ class DaylightSchedule(TimerangeSchedule): degtorad = math.pi / 180 radtodeg = 180 / math.pi - nDays = date.timetuple().tm_yday #Number of days since 01/01 + #Number of days since 01/01 + days = date.timetuple().tm_yday # Longitudinal correction longCorr = 4 * lng # calibrate for solstice - b = 2 * math.pi * (nDays - 81) / 365 + b = 2 * math.pi * (days - 81) / 365 # Equation of Time Correction eoTCorr = 9.87 * math.sin(2 * b) - 7.53 * math.cos(b) - 1.5 * math.sin(b) @@ -152,10 +155,10 @@ class DaylightSchedule(TimerangeSchedule): solarCorr = longCorr + eoTCorr # Solar declination - delta = math.asin(math.sin(23.45 * degtorad) * math.sin(b)) + declination = math.asin(math.sin(23.45 * degtorad) * math.sin(b)) - sunrise = 12 - math.acos(-math.tan(lat * degtorad) * math.tan(delta)) * radtodeg / 15 - solarCorr / 60 - sunset = 12 + math.acos(-math.tan(lat * degtorad) * math.tan(delta)) * radtodeg / 15 - solarCorr / 60 + sunrise = 12 - math.acos(-math.tan(lat * degtorad) * math.tan(declination)) * radtodeg / 15 - solarCorr / 60 + sunset = 12 + math.acos(-math.tan(lat * degtorad) * math.tan(declination)) * radtodeg / 15 - solarCorr / 60 midnight = datetime.combine(date, datetime.min.time()) sunrise = midnight + timedelta(hours=sunrise) @@ -164,24 +167,48 @@ class DaylightSchedule(TimerangeSchedule): return sunrise, sunset - def getEntry(self, t, profile): + def getEntry(self, t, profile, useGreyline): now = datetime.utcnow() date = now.date() if t == "day": sunrise, sunset = self.getSunTimes(date) if sunset < now: sunrise, sunset = self.getSunTimes(date + timedelta(days=1)) - return DatetimeScheduleEntry(sunrise, sunset, profile) + if useGreyline: + sunrise += DaylightSchedule.greyLineTime + sunset -= DaylightSchedule.greyLineTime + return [ DatetimeScheduleEntry(sunrise, sunset, profile) ] elif t == "night": sunrise, _ = self.getSunTimes(date) _, sunset = self.getSunTimes(date - timedelta(days=1)) if sunrise < now: sunrise, _ = self.getSunTimes(date + timedelta(days=1)) _, sunset = self.getSunTimes(date) - return DatetimeScheduleEntry(sunset, sunrise, profile) + if useGreyline: + sunrise -= DaylightSchedule.greyLineTime + sunset += DaylightSchedule.greyLineTime + return [ DatetimeScheduleEntry(sunset, sunrise, profile) ] + elif t == "greyline": + sunrise, sunset = self.getSunTimes(date) + if sunrise < now + DaylightSchedule.greyLineTime: + sunrise, _ = self.getSunTimes(date + timedelta(days=1)) + if sunset < now + DaylightSchedule.greyLineTime: + _, sunset = self.getSunTimes(date + timedelta(days=1)) + return [ + DatetimeScheduleEntry( + sunrise - DaylightSchedule.greyLineTime, sunrise + DaylightSchedule.greyLineTime, profile + ), + DatetimeScheduleEntry( + sunset - DaylightSchedule.greyLineTime, sunset + DaylightSchedule.greyLineTime, profile + ), + ] def getEntries(self): - return [self.getEntry(t, profile) for t, profile in self.schedule.items()] + # greyline is optional, it its set it will shorten the other profiles + useGreyline = "greyline" in self.schedule + entries = [e for t, profile in self.schedule.items() for e in self.getEntry(t, profile, useGreyline)] + logger.debug([str(e) for e in entries]) + return entries class ServiceScheduler(object): From f06f1265d8548240293c999dd549c39300c21560 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 Jan 2020 18:54:53 +0100 Subject: [PATCH 006/475] just calculate today's schedule, makes things much easiear --- owrx/service/schedule.py | 54 +++++++++++++--------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/owrx/service/schedule.py b/owrx/service/schedule.py index d5d7388..0397890 100644 --- a/owrx/service/schedule.py +++ b/owrx/service/schedule.py @@ -167,46 +167,26 @@ class DaylightSchedule(TimerangeSchedule): return sunrise, sunset - def getEntry(self, t, profile, useGreyline): - now = datetime.utcnow() - date = now.date() - if t == "day": - sunrise, sunset = self.getSunTimes(date) - if sunset < now: - sunrise, sunset = self.getSunTimes(date + timedelta(days=1)) - if useGreyline: - sunrise += DaylightSchedule.greyLineTime - sunset -= DaylightSchedule.greyLineTime - return [ DatetimeScheduleEntry(sunrise, sunset, profile) ] - elif t == "night": - sunrise, _ = self.getSunTimes(date) - _, sunset = self.getSunTimes(date - timedelta(days=1)) - if sunrise < now: - sunrise, _ = self.getSunTimes(date + timedelta(days=1)) - _, sunset = self.getSunTimes(date) - if useGreyline: - sunrise -= DaylightSchedule.greyLineTime - sunset += DaylightSchedule.greyLineTime - return [ DatetimeScheduleEntry(sunset, sunrise, profile) ] - elif t == "greyline": - sunrise, sunset = self.getSunTimes(date) - if sunrise < now + DaylightSchedule.greyLineTime: - sunrise, _ = self.getSunTimes(date + timedelta(days=1)) - if sunset < now + DaylightSchedule.greyLineTime: - _, sunset = self.getSunTimes(date + timedelta(days=1)) - return [ - DatetimeScheduleEntry( - sunrise - DaylightSchedule.greyLineTime, sunrise + DaylightSchedule.greyLineTime, profile - ), - DatetimeScheduleEntry( - sunset - DaylightSchedule.greyLineTime, sunset + DaylightSchedule.greyLineTime, profile - ), - ] - def getEntries(self): + date = datetime.utcnow().date() # greyline is optional, it its set it will shorten the other profiles useGreyline = "greyline" in self.schedule - entries = [e for t, profile in self.schedule.items() for e in self.getEntry(t, profile, useGreyline)] + entries = [] + + sunrise, sunset = self.getSunTimes(date) + delta = DaylightSchedule.greyLineTime if useGreyline else timedelta() + + entries += [ + DatetimeScheduleEntry(datetime.combine(date, datetime.min.time()), sunrise - delta, self.schedule["night"]), + DatetimeScheduleEntry(sunrise + delta, sunset - delta, self.schedule["day"]), + DatetimeScheduleEntry(sunset + delta, datetime.combine(date, datetime.max.time()), self.schedule["night"]), + ] + if useGreyline: + entries += [ + DatetimeScheduleEntry(sunrise - delta, sunrise + delta, self.schedule["greyline"]), + DatetimeScheduleEntry(sunset - delta, sunrise + delta, self.schedule["greyline"]), + ] + logger.debug([str(e) for e in entries]) return entries From 5b9344dee9fc36b3d2a84350effa3870bffd8f09 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 20 Jan 2020 17:29:32 +0100 Subject: [PATCH 007/475] fix evening greyline --- owrx/service/schedule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/service/schedule.py b/owrx/service/schedule.py index 0397890..f16d9ab 100644 --- a/owrx/service/schedule.py +++ b/owrx/service/schedule.py @@ -184,7 +184,7 @@ class DaylightSchedule(TimerangeSchedule): if useGreyline: entries += [ DatetimeScheduleEntry(sunrise - delta, sunrise + delta, self.schedule["greyline"]), - DatetimeScheduleEntry(sunset - delta, sunrise + delta, self.schedule["greyline"]), + DatetimeScheduleEntry(sunset - delta, sunset + delta, self.schedule["greyline"]), ] logger.debug([str(e) for e in entries]) From b63a991008830785857a6b9ae68d326cfb72e850 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 24 Jan 2020 23:29:25 +0100 Subject: [PATCH 008/475] redo the scheduling so it works close to the dateline, too --- owrx/service/schedule.py | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/owrx/service/schedule.py b/owrx/service/schedule.py index f16d9ab..509a26f 100644 --- a/owrx/service/schedule.py +++ b/owrx/service/schedule.py @@ -168,24 +168,36 @@ class DaylightSchedule(TimerangeSchedule): return sunrise, sunset def getEntries(self): - date = datetime.utcnow().date() + now = datetime.utcnow() + date = now.date() # greyline is optional, it its set it will shorten the other profiles useGreyline = "greyline" in self.schedule entries = [] - sunrise, sunset = self.getSunTimes(date) delta = DaylightSchedule.greyLineTime if useGreyline else timedelta() + events = [] + # we need to start yesterday for longitudes close to the date line + offset = -1 + while len(events) < 1: + sunrise, sunset = self.getSunTimes(date + timedelta(days=offset)) + offset += 1 + events += [{"type": "sunrise", "time": sunrise}, {"type": "sunset", "time": sunset}] + # keep only events in the future + events = [v for v in events if v["time"] + delta > now] + events.sort(key=lambda e: e["time"]) - entries += [ - DatetimeScheduleEntry(datetime.combine(date, datetime.min.time()), sunrise - delta, self.schedule["night"]), - DatetimeScheduleEntry(sunrise + delta, sunset - delta, self.schedule["day"]), - DatetimeScheduleEntry(sunset + delta, datetime.combine(date, datetime.max.time()), self.schedule["night"]), - ] - if useGreyline: - entries += [ - DatetimeScheduleEntry(sunrise - delta, sunrise + delta, self.schedule["greyline"]), - DatetimeScheduleEntry(sunset - delta, sunset + delta, self.schedule["greyline"]), - ] + previousEvent = None + for event in events: + # night profile _until_ sunrise, day profile _until_ sunset + stype = "night" if event["type"] == "sunrise" else "day" + if previousEvent is not None or event["time"] - delta > now: + start = now if previousEvent is None else previousEvent + entries.append(DatetimeScheduleEntry(start, event["time"] - delta, self.schedule[stype])) + if useGreyline: + entries.append( + DatetimeScheduleEntry(event["time"] - delta, event["time"] + delta, self.schedule["greyline"]) + ) + previousEvent = event["time"] + delta logger.debug([str(e) for e in entries]) return entries From 34312dd402e5b602fe10c38fe505ecc97ef54312 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Jan 2020 20:53:55 +0100 Subject: [PATCH 009/475] fix url hash parsing --- htdocs/openwebrx.js | 75 ++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 50cfdec..4546b9a 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1050,8 +1050,11 @@ function on_ws_recv(evt) { waterfall_auto_level_margin = config['waterfall_auto_level_margin']; waterfallColorsDefault(); - starting_mod = config['start_mod']; - starting_offset_frequency = config['start_offset_freq']; + var initial_demodulator_params = { + mod: config['start_mod'], + offset_frequency: config['start_offset_freq'] + }; + bandwidth = config['samp_rate']; center_freq = config['center_freq']; fft_size = config['fft_size']; @@ -1067,7 +1070,7 @@ function on_ws_recv(evt) { updateSquelch(); waterfall_init(); - initialize_demodulator(); + initialize_demodulator(initial_demodulator_params); bookmarks.loadLocalBookmarks(); waterfall_clear(); @@ -1489,28 +1492,37 @@ function webrx_set_param(what, value) { ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); } -var starting_offset_frequency; -var starting_mod; - function parseHash() { - var h; - if (h = window.location.hash) { - h.substring(1).split(",").forEach(function (x) { - var harr = x.split("="); - if (harr[0] === "mute") toggleMute(); - else if (harr[0] === "mod") starting_mod = harr[1]; - else if (harr[0] === "sql") { - e("openwebrx-panel-squelch").value = harr[1]; - updateSquelch(); - } - else if (harr[0] === "freq") { - console.log(parseInt(harr[1])); - console.log(center_freq); - starting_offset_frequency = parseInt(harr[1]) - center_freq; - } - }); - + if (!window.location.hash) { + return {}; } + return window.location.hash.substring(1).split(",").map(function(x) { + var harr = x.split('='); + return [harr[0], harr.slice(1).join('=')]; + }).reduce(function(params, p){ + params[p[0]] = p[1]; + return params; + }, {}); +} + +function validateHash() { + var params = parseHash(); + params = Object.keys(params).filter(function(key) { + if (key == 'freq') { + return Math.abs(params[key] - center_freq) < bandwidth; + } + return true; + }).reduce(function(p, key) { + p[key] = params[key]; + return p; + }, {}); + + if (params['freq']) { + params['offset_frequency'] = params['freq'] - center_freq; + delete params['freq']; + } + + return params; } function onAudioStart(success, apiType){ @@ -1528,14 +1540,14 @@ function onAudioStart(success, apiType){ updateVolume(); } -function initialize_demodulator() { - demodulator_analog_replace(starting_mod); - if (starting_offset_frequency) { - demodulators[0].offset_frequency = starting_offset_frequency; - tunedFrequencyDisplay.setFrequency(center_freq + starting_offset_frequency); - demodulators[0].set(); - mkscale(); - } +function initialize_demodulator(initialParams) { + mkscale(); + var params = $.extend(initialParams || {}, validateHash()); + console.info(params); + if (!params.mod) return; + demodulator_analog_replace(params.mod); + if (!params.offset_frequency) return; + demodulators[0].set_offset_frequency(params.offset_frequency); } var reconnect_timeout = false; @@ -1794,7 +1806,6 @@ function openwebrx_init() { check_top_bar_congestion(); init_header(); bookmarks = new BookmarkBar(); - parseHash(); initSliders(); } From 92254c8c4d0c87275e5d03695d378444089d31ab Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Jan 2020 21:15:05 +0100 Subject: [PATCH 010/475] update hash when demodulator params change --- htdocs/openwebrx.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 4546b9a..1065346 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -558,6 +558,7 @@ function demodulator_analog_replace(subtype, for_digital) { //this function shou demodulator_add(new Demodulator_default_analog(temp_offset, subtype)); demodulator_buttons_update(); update_digitalvoice_panels("openwebrx-panel-metadata-" + subtype); + updateHash(); } Demodulator.prototype.set_offset_frequency = function(to_what) { @@ -566,6 +567,11 @@ Demodulator.prototype.set_offset_frequency = function(to_what) { this.set(); mkenvelopes(get_visible_freq_range()); tunedFrequencyDisplay.setFrequency(center_freq + to_what); + updateHash(); +} + +Demodulator.prototype.get_offset_frequency = function() { + return this.offset_frequency; } function waterfallWidth() { @@ -1525,6 +1531,16 @@ function validateHash() { return params; } +function updateHash() { + var demod = demodulators[0]; + window.location.hash = $.map({ + freq: demod.get_offset_frequency() + center_freq, + mod: demod.subtype + }, function(value, key){ + return key + '=' + value; + }).join(','); +} + function onAudioStart(success, apiType){ divlog('Web Audio API succesfully initialized, using ' + apiType + ' API, sample rate: ' + audioEngine.getSampleRate() + " Hz"); @@ -1543,11 +1559,12 @@ function onAudioStart(success, apiType){ function initialize_demodulator(initialParams) { mkscale(); var params = $.extend(initialParams || {}, validateHash()); - console.info(params); - if (!params.mod) return; - demodulator_analog_replace(params.mod); - if (!params.offset_frequency) return; - demodulators[0].set_offset_frequency(params.offset_frequency); + if (params.mod) { + demodulator_analog_replace(params.mod); + } + if (params.offset_frequency) { + demodulators[0].set_offset_frequency(params.offset_frequency); + } } var reconnect_timeout = false; @@ -1807,6 +1824,9 @@ function openwebrx_init() { init_header(); bookmarks = new BookmarkBar(); initSliders(); + window.addEventListener('hashchange', function() { + initialize_demodulator(); + }); } function initSliders() { From 4b60b7e0463d5461458c358ce458ee6ffcc4203d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Jan 2020 22:35:44 +0100 Subject: [PATCH 011/475] frequency editor on click --- htdocs/css/openwebrx.css | 19 ++++++++++++++----- htdocs/lib/FrequencyDisplay.js | 31 ++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 556558f..349ea4d 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -311,16 +311,25 @@ input[type=range]:focus::-ms-fill-upper font-style: normal; } -#webrx-actual-freq -{ +#webrx-actual-freq { width: 100%; text-align: left; - font-size: 16pt; - font-family: 'roboto-mono'; padding: 0; margin: 0; - line-height:22px; +} +#webrx-actual-freq input { + font-family: 'roboto-mono'; + width: 98%; + box-sizing: border-box; + border: 0; + padding: 0; +} + +#webrx-actual-freq, #webrx-actual-freq input { + font-size: 16pt; + font-family: 'roboto-mono'; + line-height: 22px; } #webrx-mouse-freq diff --git a/htdocs/lib/FrequencyDisplay.js b/htdocs/lib/FrequencyDisplay.js index 0353fd8..8c749ca 100644 --- a/htdocs/lib/FrequencyDisplay.js +++ b/htdocs/lib/FrequencyDisplay.js @@ -1,8 +1,10 @@ function FrequencyDisplay(element) { this.element = $(element); this.digits = []; + this.displayContainer = $('
'); this.digitContainer = $(''); - this.element.html([this.digitContainer, $(' MHz')]); + this.displayContainer.html([this.digitContainer, $(' MHz')]); + this.element.html(this.displayContainer); this.decimalSeparator = (0.1).toLocaleString().substring(1, 2); this.setFrequency(0); } @@ -38,7 +40,7 @@ TuneableFrequencyDisplay.prototype = new FrequencyDisplay(); TuneableFrequencyDisplay.prototype.setupEvents = function() { var me = this; - this.element.on('wheel', function(e){ + me.element.on('wheel', function(e){ e.preventDefault(); e.stopPropagation(); @@ -53,7 +55,30 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() { l(newFrequency); }); }); - this.listeners = []; + me.listeners = []; + me.element.on('click', function(){ + var input = $(''); + var submit = function(){ + var freq = parseInt(input.val()); + if (!isNaN(freq)) { + me.listeners.forEach(function(l) { + l(freq); + }); + } + input.remove(); + me.displayContainer.show(); + }; + input.on('blur', submit).on('keyup', function(e){ + if (e.keyCode == 13) return submit(); + }); + input.on('click', function(e){ + e.stopPropagation(); + }); + input.val(me.frequency); + me.displayContainer.hide(); + me.element.append(input); + input.focus(); + }); }; TuneableFrequencyDisplay.prototype.onFrequencyChange = function(listener){ From 8fc981c8a07f49f03fb5242dc05b92ca51be66eb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Jan 2020 22:47:47 +0100 Subject: [PATCH 012/475] use static elements --- htdocs/index.html | 4 +-- htdocs/lib/FrequencyDisplay.js | 61 ++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index d9296c9..3fabf73 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -144,8 +144,8 @@
-
---.--- MHz
-
---.--- MHz
+
+
- + @@ -77,7 +78,15 @@ - + + + + + + + + + @@ -86,7 +95,7 @@ - + diff --git a/htdocs/lib/Js8Threads.js b/htdocs/lib/Js8Threads.js new file mode 100644 index 0000000..07e3069 --- /dev/null +++ b/htdocs/lib/Js8Threads.js @@ -0,0 +1,86 @@ +Js8Thread = function(el){ + this.messages = []; + this.el = el; +} + +Js8Thread.prototype.getAverageFrequency = function(){ + var total = this.messages.map(function(message){ + return message.freq; + }).reduce(function(t, f){ + return t + f; + }, 0); + return total / this.messages.length; +} + +Js8Thread.prototype.pushMessage = function(message) { + this.messages.push(message); + this.render(); +} + +Js8Thread.prototype.render = function() { + this.el.html( + '' + + '' + + '' + ); +} + +Js8Thread.prototype.getLatestTimestamp() { + return this.messages(this.messages.length - 1).timestamp; +} + +Js8Thread.prototype.renderMessages = function() { + res = [] + for (var i = 0; i < this.messages.length; i++) { + var msg = this.messages[i]; + if (msg.thread_type & 1) { + res.push('[ '); + } else if (i > 0 && msg.timestamp - this.messages[i - 1].timestamp > 15000) { + res.push(' ... '); + } + res.push(msg.msg); + if (msg.thread_type & 2) { + res.push(' ]'); + } + } + return res.join(''); +} + +Js8Thread.prototype.renderTimestamp = function(timestamp) { + var t = new Date(timestamp); + var pad = function (i) { + return ('' + i).padStart(2, "0"); + }; + return pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()); +} + +Js8Threader = function(el){ + this.threads = []; + this.tbody = $(el).find('tbody'); + console.info(this.tbody); +}; + +Js8Threader.prototype.findThread = function(freq) { + var matching = this.threads.filter(function(thread) { + return Math.abs(thread.getAverageFrequency() - freq) <= 5; + }); + return matching[0] || false; +} + +Js8Threader.prototype.pushMessage = function(message) { + var thread = this.findThread(message.freq); + if (!thread) { + var line = $("") + this.tbody.append(line); + var thread = new Js8Thread(line); + this.threads.push(thread); + } + thread.pushMessage(message); +} + +$.fn.js8 = function() { + if (!this.data('threader')) { + this.data('threader', new Js8Threader(this)); + } + return this.data('threader'); +} \ No newline at end of file diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 95c2008..05d4f51 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1132,8 +1132,10 @@ function on_ws_recv(evt) { case "metadata": update_metadata(json['value']); break; - case "wsjt_message": case "js8_message": + $("#openwebrx-panel-js8-message").js8().pushMessage(json['value']); + break; + case "wsjt_message": update_wsjt_panel(json['value']); break; case "dial_frequencies": @@ -2047,7 +2049,8 @@ function demodulator_digital_replace(subtype) { demodulator_buttons_update(); $('#openwebrx-panel-digimodes').attr('data-mode', subtype); toggle_panel("openwebrx-panel-digimodes", true); - toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'js8'].indexOf(subtype) >= 0); + toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(subtype) >= 0); + toggle_panel("openwebrx-panel-js8-message", subtype == "js8"); toggle_panel("openwebrx-panel-packet-message", subtype === "packet"); toggle_panel("openwebrx-panel-pocsag-message", subtype === "pocsag"); updateHash(); @@ -2141,6 +2144,7 @@ function secondary_demod_close_window() { toggle_panel("openwebrx-panel-wsjt-message", false); toggle_panel("openwebrx-panel-packet-message", false); toggle_panel("openwebrx-panel-pocsag-message", false); + toggle_panel("openwebrx-panel-js8-message", false); } function secondary_demod_waterfall_add(data) { diff --git a/owrx/connection.py b/owrx/connection.py index e513f27..7432978 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -341,6 +341,7 @@ class OpenWebRxReceiverClient(Client): "db": frame.db, "dt": frame.dt, "freq": freq + frame.freq, + "thread_type": frame.thread_type }}) From c90b415c8bdabaa3f41ebcc3a4126a11c9167003 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 Apr 2020 23:35:06 +0200 Subject: [PATCH 217/475] add scroll-to-bottom and cleanup intervals --- htdocs/css/openwebrx.css | 2 +- htdocs/lib/Js8Threads.js | 41 +++++++++++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 2ab29e3..9c939a0 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -966,7 +966,7 @@ img.openwebrx-mirror-img overflow: hidden; text-overflow: ellipsis; direction: rtl; - text-aligh: left; + text-align: left; } #openwebrx-panel-js8-message .decimal { diff --git a/htdocs/lib/Js8Threads.js b/htdocs/lib/Js8Threads.js index 07e3069..a9bc014 100644 --- a/htdocs/lib/Js8Threads.js +++ b/htdocs/lib/Js8Threads.js @@ -21,12 +21,18 @@ Js8Thread.prototype.render = function() { this.el.html( '' + '' + - '' + '' ); } -Js8Thread.prototype.getLatestTimestamp() { - return this.messages(this.messages.length - 1).timestamp; +Js8Thread.prototype.getLatestTimestamp = function() { + var startingMessages = this.messages.filter(function(m){ + return m.thread_type & 1; + }); + if (startingMessages.length) { + return startingMessages[startingMessages.length - 1].timestamp; + } + return this.messages[0].timestamp; } Js8Thread.prototype.renderMessages = function() { @@ -35,7 +41,7 @@ Js8Thread.prototype.renderMessages = function() { var msg = this.messages[i]; if (msg.thread_type & 1) { res.push('[ '); - } else if (i > 0 && msg.timestamp - this.messages[i - 1].timestamp > 15000) { + } else if (i == 0 || msg.timestamp - this.messages[i - 1].timestamp > 15000) { res.push(' ... '); } res.push(msg.msg); @@ -54,14 +60,38 @@ Js8Thread.prototype.renderTimestamp = function(timestamp) { return pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()); } +Js8Thread.prototype.purgeOldMessages = function() { + var now = new Date().getTime(); + this.messages = this.messages.filter(function(m) { + // keep messages around for 20 minutes + return now - m.timestamp < 20 * 60 * 1000; + }); + if (!this.messages.length) { + this.el.remove(); + } else { + this.render(); + } + return this.messages.length; +} + Js8Threader = function(el){ this.threads = []; this.tbody = $(el).find('tbody'); - console.info(this.tbody); + var me = this; + this.interval = setInterval(function(){ + me.purgeOldMessages(); + }, 15000); +}; + +Js8Threader.prototype.purgeOldMessages = function() { + this.threads = this.threads.filter(function(t) { + return t.purgeOldMessages(); + }); }; Js8Threader.prototype.findThread = function(freq) { var matching = this.threads.filter(function(thread) { + // max frequency deviation: 5 Hz. this may be a little tight. return Math.abs(thread.getAverageFrequency() - freq) <= 5; }); return matching[0] || false; @@ -76,6 +106,7 @@ Js8Threader.prototype.pushMessage = function(message) { this.threads.push(thread); } thread.pushMessage(message); + this.tbody.scrollTop(this.tbody[0].scrollHeight); } $.fn.js8 = function() { From 9a86bc23beed5b8d714b15ebfdc6e114026711e5 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 Apr 2020 23:36:35 +0200 Subject: [PATCH 218/475] make hackrf sleep for 1 second on restarts (device is not released immediately) --- owrx/source/hackrf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/owrx/source/hackrf.py b/owrx/source/hackrf.py index 23ffcc8..f59a448 100644 --- a/owrx/source/hackrf.py +++ b/owrx/source/hackrf.py @@ -1,5 +1,6 @@ from .direct import DirectSource from owrx.command import Option +import time class HackrfSource(DirectSource): @@ -17,3 +18,6 @@ class HackrfSource(DirectSource): def getFormatConversion(self): return ["csdr convert_s8_f"] + + def sleepOnRestart(self): + time.sleep(1) From 5ba77012a71b63cc47c4b0feee028281d5ae694a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 19 Apr 2020 23:42:36 +0200 Subject: [PATCH 219/475] update js8py library --- docker/scripts/install-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index dda0376..1f195d7 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -26,7 +26,7 @@ apk add --no-cache --virtual .build-deps $BUILD_PACKAGES git clone https://github.com/jketterl/js8py.git pushd js8py -git checkout b55b6c7668c353b9f1af507c505b8a774951dee6 +git checkout 888e62be375316882ad2b2ac8e396c3bf857b6fc python3 setup.py install popd From f474ab94d2febaf19390b50f0293bd8c1fa9a138 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 20 Apr 2020 18:31:45 +0200 Subject: [PATCH 220/475] close threads when ending message has been received --- htdocs/lib/Js8Threads.js | 46 +++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/htdocs/lib/Js8Threads.js b/htdocs/lib/Js8Threads.js index a9bc014..b191061 100644 --- a/htdocs/lib/Js8Threads.js +++ b/htdocs/lib/Js8Threads.js @@ -1,7 +1,7 @@ Js8Thread = function(el){ this.messages = []; this.el = el; -} +}; Js8Thread.prototype.getAverageFrequency = function(){ var total = this.messages.map(function(message){ @@ -10,12 +10,12 @@ Js8Thread.prototype.getAverageFrequency = function(){ return t + f; }, 0); return total / this.messages.length; -} +}; Js8Thread.prototype.pushMessage = function(message) { this.messages.push(message); this.render(); -} +}; Js8Thread.prototype.render = function() { this.el.html( @@ -23,34 +23,36 @@ Js8Thread.prototype.render = function() { '' + '' ); -} +}; Js8Thread.prototype.getLatestTimestamp = function() { - var startingMessages = this.messages.filter(function(m){ - return m.thread_type & 1; - }); - if (startingMessages.length) { - return startingMessages[startingMessages.length - 1].timestamp; - } return this.messages[0].timestamp; -} +}; + +Js8Thread.prototype.isOpen = function() { + if (!this.messages.length) return true; + var last_message = this.messages[this.messages.length - 1]; + return (last_message.thread_type & 2) === 0; +}; Js8Thread.prototype.renderMessages = function() { - res = [] + var res = []; for (var i = 0; i < this.messages.length; i++) { var msg = this.messages[i]; if (msg.thread_type & 1) { res.push('[ '); - } else if (i == 0 || msg.timestamp - this.messages[i - 1].timestamp > 15000) { + } else if (i === 0 || msg.timestamp - this.messages[i - 1].timestamp > 15000) { res.push(' ... '); } res.push(msg.msg); if (msg.thread_type & 2) { res.push(' ]'); + } else if (i === this.messages.length -1) { + res.push(' ... '); } } return res.join(''); -} +}; Js8Thread.prototype.renderTimestamp = function(timestamp) { var t = new Date(timestamp); @@ -58,7 +60,7 @@ Js8Thread.prototype.renderTimestamp = function(timestamp) { return ('' + i).padStart(2, "0"); }; return pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds()); -} +}; Js8Thread.prototype.purgeOldMessages = function() { var now = new Date().getTime(); @@ -72,7 +74,7 @@ Js8Thread.prototype.purgeOldMessages = function() { this.render(); } return this.messages.length; -} +}; Js8Threader = function(el){ this.threads = []; @@ -92,26 +94,26 @@ Js8Threader.prototype.purgeOldMessages = function() { Js8Threader.prototype.findThread = function(freq) { var matching = this.threads.filter(function(thread) { // max frequency deviation: 5 Hz. this may be a little tight. - return Math.abs(thread.getAverageFrequency() - freq) <= 5; + return thread.isOpen() && Math.abs(thread.getAverageFrequency() - freq) <= 5; }); return matching[0] || false; -} +}; Js8Threader.prototype.pushMessage = function(message) { var thread = this.findThread(message.freq); if (!thread) { - var line = $("") + var line = $(""); this.tbody.append(line); - var thread = new Js8Thread(line); + thread = new Js8Thread(line); this.threads.push(thread); } thread.pushMessage(message); this.tbody.scrollTop(this.tbody[0].scrollHeight); -} +}; $.fn.js8 = function() { if (!this.data('threader')) { this.data('threader', new Js8Threader(this)); } return this.data('threader'); -} \ No newline at end of file +}; \ No newline at end of file From 0c12d07a26fb1158d108bab3439f34e73ee5ca80 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 20 Apr 2020 22:07:21 +0200 Subject: [PATCH 221/475] finalize visual message representation --- htdocs/css/openwebrx.css | 16 +++++++++++----- htdocs/lib/Js8Threads.js | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 9c939a0..b5693c8 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -960,13 +960,19 @@ img.openwebrx-mirror-img } #openwebrx-panel-js8-message .message { - width: 470px; - max-width: 470px; + width: 465px; + max-width: 465px; +} + +#openwebrx-panel-js8-message td.message { white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; - direction: rtl; - text-align: left; + display: flex; + flex-direction: row-reverse; +} + +#openwebrx-panel-js8-message .message div { + flex: 1; } #openwebrx-panel-js8-message .decimal { diff --git a/htdocs/lib/Js8Threads.js b/htdocs/lib/Js8Threads.js index b191061..fcecfa6 100644 --- a/htdocs/lib/Js8Threads.js +++ b/htdocs/lib/Js8Threads.js @@ -21,7 +21,7 @@ Js8Thread.prototype.render = function() { this.el.html( '' + '' + - '' + '' ); }; From aa4362fe9fdba1a8fb49f453826fb5c912bbb81b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 20 Apr 2020 22:12:33 +0200 Subject: [PATCH 222/475] add js8 to the changelog --- CHANGELOG.md | 3 +++ debian/changelog | 2 ++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ae9f0..23dab09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ - Added support for bias tee control on rtl_sdr devices - All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC - `rtl_sdr` type now also supports the `direct_sampling` option +- Added decoding implementation for for digimode "JS8Call" + (requires an installation of [js8call](http://js8call.com/) and + [the js8py library](https://github.com/jketterl/js8py)) **0.18.0** - Support for SoapyRemote diff --git a/debian/changelog b/debian/changelog index ded59bd..b4497f7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -19,6 +19,8 @@ openwebrx (0.19.0) UNRELEASED; urgency=low * Added support for bias tee control on rtl_sdr devices * All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC * `rtl_sdr` type now also supports the `direct_sampling` option + * Added decoding implementation for for digimode "JS8Call" (requires an + installation of js8call and the js8py library) -- Jakob Ketterl Thu, 20 Feb 2020 21:01:00 +0000 From 681a583711569a9928fb9a949acb5c358a0aca6c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 21 Apr 2020 18:11:07 +0200 Subject: [PATCH 223/475] always begin a new message if the flag says so --- htdocs/lib/Js8Threads.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/htdocs/lib/Js8Threads.js b/htdocs/lib/Js8Threads.js index fcecfa6..12997a6 100644 --- a/htdocs/lib/Js8Threads.js +++ b/htdocs/lib/Js8Threads.js @@ -100,7 +100,11 @@ Js8Threader.prototype.findThread = function(freq) { }; Js8Threader.prototype.pushMessage = function(message) { - var thread = this.findThread(message.freq); + var thread; + // only look for exising threads if the message is not a starting message + if ((message.thread_type & 1) === 0) { + thread = this.findThread(message.freq); + } if (!thread) { var line = $(""); this.tbody.append(line); From 0a16500133c17bf9257a6f059cbaefbaea0d4402 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 21 Apr 2020 19:24:57 +0200 Subject: [PATCH 224/475] get avatar path from pkg_resources, refs #108 --- owrx/controllers/status.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/owrx/controllers/status.py b/owrx/controllers/status.py index 1c73f66..61f7102 100644 --- a/owrx/controllers/status.py +++ b/owrx/controllers/status.py @@ -5,6 +5,7 @@ from owrx.sdr import SdrService from owrx.config import Config import os import json +import pkg_resources class StatusController(Controller): @@ -12,6 +13,7 @@ class StatusController(Controller): pm = Config.get() # convert to old format gps = (pm["receiver_gps"]["lat"], pm["receiver_gps"]["lon"]) + avatar_path = pkg_resources.resource_filename("htdocs", "gfx/openwebrx-avatar.png") # TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna vars = { "status": "active", @@ -23,7 +25,7 @@ class StatusController(Controller): "asl": pm["receiver_asl"], "loc": pm["receiver_location"], "sw_version": openwebrx_version, - "avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png"), + "avatar_ctime": os.path.getctime(avatar_path), } self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()])) From 4f07c62cc9364969fb4adf7e74a4e05d821db801 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Tue, 21 Apr 2020 21:00:16 +0200 Subject: [PATCH 225/475] use the latest available thread --- htdocs/lib/Js8Threads.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/htdocs/lib/Js8Threads.js b/htdocs/lib/Js8Threads.js index 12997a6..d665b10 100644 --- a/htdocs/lib/Js8Threads.js +++ b/htdocs/lib/Js8Threads.js @@ -96,6 +96,9 @@ Js8Threader.prototype.findThread = function(freq) { // max frequency deviation: 5 Hz. this may be a little tight. return thread.isOpen() && Math.abs(thread.getAverageFrequency() - freq) <= 5; }); + matching.sort(function(a, b){ + return b.getLatestTimestamp() - a.getLatestTimestamp(); + }); return matching[0] || false; }; From 78ccaa7d65ba3fb23b8729b907756c8a08e98c64 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 22 Apr 2020 18:28:45 +0200 Subject: [PATCH 226/475] access regex groups in python 3.5 compatible way, closes #109 --- owrx/feature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/feature.py b/owrx/feature.py index df2434b..24cd547 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -252,7 +252,7 @@ class FeatureDetector(object): for line in process.stdout: matches = factory_regex.match(line.decode()) if matches: - drivers = [s.strip() for s in matches[1].split(", ")] + drivers = [s.strip() for s in matches.group(1).split(", ")] return driver in drivers except FileNotFoundError: From 0120b33a25767b40506e91abbefb2957506e40a3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 22 Apr 2020 23:53:19 +0200 Subject: [PATCH 227/475] refactor chopper out of wsjt --- csdr/csdr.py | 23 ++--- owrx/audio.py | 226 +++++++++++++++++++++++++++++++++++++++++++++++++ owrx/js8.py | 4 +- owrx/wsjt.py | 227 ++------------------------------------------------ 4 files changed, 249 insertions(+), 231 deletions(-) create mode 100644 owrx/audio.py diff --git a/csdr/csdr.py b/csdr/csdr.py index ac4dee3..07feb01 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -29,8 +29,9 @@ import math from functools import partial from owrx.kiss import KissClient, DirewolfConfig -from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper -from owrx.js8 import Js8Chopper +from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile +from owrx.js8 import Js8NormalProfile +from owrx.audio import AudioChopper import logging @@ -450,23 +451,23 @@ class dsp(object): if self.isWsjtMode(): smd = self.get_secondary_demodulator() - chopper_cls = None + chopper_profile = None output_name = "wsjt_demod" if smd == "ft8": - chopper_cls = Ft8Chopper + chopper_profile = Ft8Profile() elif smd == "wspr": - chopper_cls = WsprChopper + chopper_profile = WsprProfile() elif smd == "jt65": - chopper_cls = Jt65Chopper + chopper_profile = Jt65Profile() elif smd == "jt9": - chopper_cls = Jt9Chopper + chopper_profile = Jt9Profile() elif smd == "ft4": - chopper_cls = Ft4Chopper + chopper_profile = Ft4Profile() elif smd == "js8": - chopper_cls = Js8Chopper + chopper_profile = Js8NormalProfile() output_name = "js8_demod" - if chopper_cls is not None: - chopper = chopper_cls(self, self.secondary_process_demod.stdout) + if chopper_profile is not None: + chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile) chopper.start() self.output.send_output(output_name, chopper.read) elif self.isPacket(): diff --git a/owrx/audio.py b/owrx/audio.py new file mode 100644 index 0000000..90b22a5 --- /dev/null +++ b/owrx/audio.py @@ -0,0 +1,226 @@ +from abc import ABC, ABCMeta, abstractmethod +from owrx.config import Config +from owrx.metrics import Metrics, CounterMetric, DirectMetric +import threading +import wave +import subprocess +import os +from multiprocessing.connection import Pipe +from datetime import datetime, timedelta +from queue import Queue, Full + + +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class QueueJob(object): + def __init__(self, decoder, file, freq): + self.decoder = decoder + self.file = file + self.freq = freq + + def run(self): + self.decoder.decode(self) + + +class QueueWorker(threading.Thread): + def __init__(self, queue): + self.queue = queue + self.doRun = True + super().__init__(daemon=True) + + def run(self) -> None: + while self.doRun: + job = self.queue.get() + try: + job.run() + except Exception: + logger.exception("failed to decode job") + self.queue.onError() + self.queue.task_done() + + +class DecoderQueue(Queue): + sharedInstance = None + creationLock = threading.Lock() + + @staticmethod + def getSharedInstance(): + with DecoderQueue.creationLock: + if DecoderQueue.sharedInstance is None: + pm = Config.get() + DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["wsjt_queue_length"], workers=pm["wsjt_queue_workers"]) + return DecoderQueue.sharedInstance + + def __init__(self, maxsize, workers): + super().__init__(maxsize) + metrics = Metrics.getSharedInstance() + metrics.addMetric("wsjt.queue.length", DirectMetric(self.qsize)) + self.inCounter = CounterMetric() + metrics.addMetric("wsjt.queue.in", self.inCounter) + self.outCounter = CounterMetric() + metrics.addMetric("wsjt.queue.out", self.outCounter) + self.overflowCounter = CounterMetric() + metrics.addMetric("wsjt.queue.overflow", self.overflowCounter) + self.errorCounter = CounterMetric() + metrics.addMetric("wsjt.queue.error", self.errorCounter) + self.workers = [self.newWorker() for _ in range(0, workers)] + + def put(self, item, **kwars): + self.inCounter.inc() + try: + super(DecoderQueue, self).put(item, block=False) + except Full: + self.overflowCounter.inc() + raise + + def get(self, **kwargs): + # super.get() is blocking, so it would mess up the stats to inc() first + out = super(DecoderQueue, self).get(**kwargs) + self.outCounter.inc() + return out + + def newWorker(self): + worker = QueueWorker(self) + worker.start() + return worker + + def onError(self): + self.errorCounter.inc() + + +class AudioChopperProfile(ABC): + @abstractmethod + def getInterval(self): + pass + + @abstractmethod + def getFileTimestampFormat(self): + pass + + @abstractmethod + def decoder_commandline(self, file): + pass + + def decoding_depth(self, mode): + pm = Config.get() + # mode-specific setting? + if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]: + return pm["wsjt_decoding_depths"][mode] + # return global default + if "wsjt_decoding_depth" in pm: + return pm["wsjt_decoding_depth"] + # default when no setting is provided + return 3 + + +class AudioChopper(threading.Thread, metaclass=ABCMeta): + def __init__(self, dsp, source, profile: AudioChopperProfile): + self.dsp = dsp + self.source = source + self.profile = profile + self.tmp_dir = Config.get()["temporary_directory"] + self.wavefile = None + self.wavefilename = None + self.switchingLock = threading.Lock() + self.timer = None + (self.outputReader, self.outputWriter) = Pipe() + self.doRun = True + super().__init__() + + def getWaveFile(self): + filename = "{tmp_dir}/openwebrx-audiochopper-{id}-{timestamp}.wav".format( + tmp_dir=self.tmp_dir, id=id(self), timestamp=datetime.utcnow().strftime(self.profile.getFileTimestampFormat()) + ) + wavefile = wave.open(filename, "wb") + wavefile.setnchannels(1) + wavefile.setsampwidth(2) + wavefile.setframerate(12000) + return filename, wavefile + + def getNextDecodingTime(self): + t = datetime.utcnow() + zeroed = t.replace(minute=0, second=0, microsecond=0) + delta = t - zeroed + interval = self.profile.getInterval() + seconds = (int(delta.total_seconds() / interval) + 1) * interval + t = zeroed + timedelta(seconds=seconds) + logger.debug("scheduling: {0}".format(t)) + return t + + def cancelTimer(self): + if self.timer: + self.timer.cancel() + self.timer = None + + def _scheduleNextSwitch(self): + self.cancelTimer() + if self.doRun: + delta = self.getNextDecodingTime() - datetime.utcnow() + self.timer = threading.Timer(delta.total_seconds(), self.switchFiles) + self.timer.start() + + def switchFiles(self): + self.switchingLock.acquire() + file = self.wavefile + filename = self.wavefilename + (self.wavefilename, self.wavefile) = self.getWaveFile() + self.switchingLock.release() + + file.close() + try: + DecoderQueue.getSharedInstance().put(QueueJob(self, filename, self.dsp.get_operating_freq())) + except Full: + logger.warning("wsjt decoding queue overflow; dropping one file") + os.unlink(filename) + self._scheduleNextSwitch() + + def decode(self, job: QueueJob): + logger.debug("processing file %s", job.file) + decoder = subprocess.Popen( + ["nice", "-n", "10"] + self.profile.decoder_commandline(job.file), + stdout=subprocess.PIPE, + cwd=self.tmp_dir, + close_fds=True, + ) + for line in decoder.stdout: + self.outputWriter.send((job.freq, line)) + try: + rc = decoder.wait(timeout=10) + if rc != 0: + logger.warning("decoder return code: %i", rc) + except subprocess.TimeoutExpired: + logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid) + decoder.kill() + os.unlink(job.file) + + def run(self) -> None: + logger.debug("WSJT chopper starting up") + (self.wavefilename, self.wavefile) = self.getWaveFile() + self._scheduleNextSwitch() + while self.doRun: + data = self.source.read(256) + if data is None or (isinstance(data, bytes) and len(data) == 0): + self.doRun = False + else: + self.switchingLock.acquire() + self.wavefile.writeframes(data) + self.switchingLock.release() + + logger.debug("WSJT chopper shutting down") + self.outputReader.close() + self.outputWriter.close() + self.cancelTimer() + try: + os.unlink(self.wavefilename) + except Exception: + logger.exception("error removing undecoded file") + + def read(self): + try: + return self.outputReader.recv() + except EOFError: + return None diff --git a/owrx/js8.py b/owrx/js8.py index 796e4eb..303a273 100644 --- a/owrx/js8.py +++ b/owrx/js8.py @@ -1,4 +1,4 @@ -from .wsjt import WsjtChopper +from .audio import AudioChopperProfile from .parser import Parser import re from js8py import Js8 @@ -12,7 +12,7 @@ import logging logger = logging.getLogger(__name__) -class Js8Chopper(WsjtChopper): +class Js8NormalProfile(AudioChopperProfile): def getInterval(self): return 15 diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 982e475..475e3b3 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -1,17 +1,11 @@ -import threading -import wave -from datetime import datetime, timedelta, timezone -import subprocess -import os -from multiprocessing.connection import Pipe +from datetime import datetime, timezone from owrx.map import Map, LocatorLocation import re -from queue import Queue, Full -from owrx.config import Config -from owrx.metrics import Metrics, CounterMetric, DirectMetric +from owrx.metrics import Metrics, CounterMetric from owrx.pskreporter import PskReporter from owrx.parser import Parser -from abc import ABC, ABCMeta, abstractmethod +from owrx.audio import AudioChopperProfile +from abc import ABC, abstractmethod import logging @@ -19,210 +13,7 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -class QueueJob(object): - def __init__(self, decoder, file, freq): - self.decoder = decoder - self.file = file - self.freq = freq - - def run(self): - self.decoder.decode(self) - - -class WsjtQueueWorker(threading.Thread): - def __init__(self, queue): - self.queue = queue - self.doRun = True - super().__init__(daemon=True) - - def run(self) -> None: - while self.doRun: - job = self.queue.get() - try: - job.run() - except Exception: - logger.exception("failed to decode job") - self.queue.onError() - self.queue.task_done() - - -class WsjtQueue(Queue): - sharedInstance = None - creationLock = threading.Lock() - - @staticmethod - def getSharedInstance(): - with WsjtQueue.creationLock: - if WsjtQueue.sharedInstance is None: - pm = Config.get() - WsjtQueue.sharedInstance = WsjtQueue(maxsize=pm["wsjt_queue_length"], workers=pm["wsjt_queue_workers"]) - return WsjtQueue.sharedInstance - - def __init__(self, maxsize, workers): - super().__init__(maxsize) - metrics = Metrics.getSharedInstance() - metrics.addMetric("wsjt.queue.length", DirectMetric(self.qsize)) - self.inCounter = CounterMetric() - metrics.addMetric("wsjt.queue.in", self.inCounter) - self.outCounter = CounterMetric() - metrics.addMetric("wsjt.queue.out", self.outCounter) - self.overflowCounter = CounterMetric() - metrics.addMetric("wsjt.queue.overflow", self.overflowCounter) - self.errorCounter = CounterMetric() - metrics.addMetric("wsjt.queue.error", self.errorCounter) - self.workers = [self.newWorker() for _ in range(0, workers)] - - def put(self, item): - self.inCounter.inc() - try: - super(WsjtQueue, self).put(item, block=False) - except Full: - self.overflowCounter.inc() - raise - - def get(self, **kwargs): - # super.get() is blocking, so it would mess up the stats to inc() first - out = super(WsjtQueue, self).get(**kwargs) - self.outCounter.inc() - return out - - def newWorker(self): - worker = WsjtQueueWorker(self) - worker.start() - return worker - - def onError(self): - self.errorCounter.inc() - - -class WsjtChopper(threading.Thread, metaclass=ABCMeta): - def __init__(self, dsp, source): - self.dsp = dsp - self.source = source - self.tmp_dir = Config.get()["temporary_directory"] - (self.wavefilename, self.wavefile) = self.getWaveFile() - self.switchingLock = threading.Lock() - self.timer = None - (self.outputReader, self.outputWriter) = Pipe() - self.doRun = True - super().__init__() - - @abstractmethod - def getInterval(self): - pass - - @abstractmethod - def getFileTimestampFormat(self): - pass - - def getWaveFile(self): - filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format( - tmp_dir=self.tmp_dir, id=id(self), timestamp=datetime.utcnow().strftime(self.getFileTimestampFormat()) - ) - wavefile = wave.open(filename, "wb") - wavefile.setnchannels(1) - wavefile.setsampwidth(2) - wavefile.setframerate(12000) - return filename, wavefile - - def getNextDecodingTime(self): - t = datetime.utcnow() - zeroed = t.replace(minute=0, second=0, microsecond=0) - delta = t - zeroed - interval = self.getInterval() - seconds = (int(delta.total_seconds() / interval) + 1) * interval - t = zeroed + timedelta(seconds=seconds) - logger.debug("scheduling: {0}".format(t)) - return t - - def cancelTimer(self): - if self.timer: - self.timer.cancel() - - def _scheduleNextSwitch(self): - if self.doRun: - delta = self.getNextDecodingTime() - datetime.utcnow() - self.timer = threading.Timer(delta.total_seconds(), self.switchFiles) - self.timer.start() - - def switchFiles(self): - self.switchingLock.acquire() - file = self.wavefile - filename = self.wavefilename - (self.wavefilename, self.wavefile) = self.getWaveFile() - self.switchingLock.release() - - file.close() - try: - WsjtQueue.getSharedInstance().put(QueueJob(self, filename, self.dsp.get_operating_freq())) - except Full: - logger.warning("wsjt decoding queue overflow; dropping one file") - os.unlink(filename) - self._scheduleNextSwitch() - - @abstractmethod - def decoder_commandline(self, file): - pass - - def decode(self, job: QueueJob): - logger.debug("processing file %s", job.file) - decoder = subprocess.Popen( - ["nice", "-n", "10"] + self.decoder_commandline(job.file), - stdout=subprocess.PIPE, - cwd=self.tmp_dir, - close_fds=True, - ) - for line in decoder.stdout: - self.outputWriter.send((job.freq, line)) - try: - rc = decoder.wait(timeout=10) - if rc != 0: - logger.warning("decoder return code: %i", rc) - except subprocess.TimeoutExpired: - logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid) - decoder.kill() - os.unlink(job.file) - - def run(self) -> None: - logger.debug("WSJT chopper starting up") - self._scheduleNextSwitch() - while self.doRun: - data = self.source.read(256) - if data is None or (isinstance(data, bytes) and len(data) == 0): - self.doRun = False - else: - self.switchingLock.acquire() - self.wavefile.writeframes(data) - self.switchingLock.release() - - logger.debug("WSJT chopper shutting down") - self.outputReader.close() - self.outputWriter.close() - self.cancelTimer() - try: - os.unlink(self.wavefilename) - except Exception: - logger.exception("error removing undecoded file") - - def read(self): - try: - return self.outputReader.recv() - except EOFError: - return None - - def decoding_depth(self, mode): - pm = Config.get() - # mode-specific setting? - if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]: - return pm["wsjt_decoding_depths"][mode] - # return global default - if "wsjt_decoding_depth" in pm: - return pm["wsjt_decoding_depth"] - # default when no setting is provided - return 3 - - -class Ft8Chopper(WsjtChopper): +class Ft8Profile(AudioChopperProfile): def getInterval(self): return 15 @@ -233,7 +24,7 @@ class Ft8Chopper(WsjtChopper): return ["jt9", "--ft8", "-d", str(self.decoding_depth("ft8")), file] -class WsprChopper(WsjtChopper): +class WsprProfile(AudioChopperProfile): def getInterval(self): return 120 @@ -248,7 +39,7 @@ class WsprChopper(WsjtChopper): return cmd -class Jt65Chopper(WsjtChopper): +class Jt65Profile(AudioChopperProfile): def getInterval(self): return 60 @@ -259,7 +50,7 @@ class Jt65Chopper(WsjtChopper): return ["jt9", "--jt65", "-d", str(self.decoding_depth("jt65")), file] -class Jt9Chopper(WsjtChopper): +class Jt9Profile(AudioChopperProfile): def getInterval(self): return 60 @@ -270,7 +61,7 @@ class Jt9Chopper(WsjtChopper): return ["jt9", "--jt9", "-d", str(self.decoding_depth("jt9")), file] -class Ft4Chopper(WsjtChopper): +class Ft4Profile(AudioChopperProfile): def getInterval(self): return 7.5 From 5ab2f02f63e97efbf1035161e99a8da4edf78c77 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 23 Apr 2020 00:21:59 +0200 Subject: [PATCH 228/475] multi-profile decoding --- owrx/audio.py | 64 +++++++++++++++++++++++++++++++++------------------ owrx/js8.py | 59 ++++++++++++++++++++++++----------------------- owrx/wsjt.py | 54 +++++++++++++++++++++---------------------- 3 files changed, 99 insertions(+), 78 deletions(-) diff --git a/owrx/audio.py b/owrx/audio.py index 90b22a5..4e81f06 100644 --- a/owrx/audio.py +++ b/owrx/audio.py @@ -5,7 +5,7 @@ import threading import wave import subprocess import os -from multiprocessing.connection import Pipe +from multiprocessing.connection import Pipe, wait from datetime import datetime, timedelta from queue import Queue, Full @@ -13,7 +13,7 @@ from queue import Queue, Full import logging logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) +#logger.setLevel(logging.INFO) class QueueJob(object): @@ -117,7 +117,7 @@ class AudioChopperProfile(ABC): return 3 -class AudioChopper(threading.Thread, metaclass=ABCMeta): +class AudioWriter(object): def __init__(self, dsp, source, profile: AudioChopperProfile): self.dsp = dsp self.source = source @@ -128,12 +128,12 @@ class AudioChopper(threading.Thread, metaclass=ABCMeta): self.switchingLock = threading.Lock() self.timer = None (self.outputReader, self.outputWriter) = Pipe() - self.doRun = True - super().__init__() def getWaveFile(self): filename = "{tmp_dir}/openwebrx-audiochopper-{id}-{timestamp}.wav".format( - tmp_dir=self.tmp_dir, id=id(self), timestamp=datetime.utcnow().strftime(self.profile.getFileTimestampFormat()) + tmp_dir=self.tmp_dir, + id=id(self.profile), + timestamp=datetime.utcnow().strftime(self.profile.getFileTimestampFormat()), ) wavefile = wave.open(filename, "wb") wavefile.setnchannels(1) @@ -158,10 +158,9 @@ class AudioChopper(threading.Thread, metaclass=ABCMeta): def _scheduleNextSwitch(self): self.cancelTimer() - if self.doRun: - delta = self.getNextDecodingTime() - datetime.utcnow() - self.timer = threading.Timer(delta.total_seconds(), self.switchFiles) - self.timer.start() + delta = self.getNextDecodingTime() - datetime.utcnow() + self.timer = threading.Timer(delta.total_seconds(), self.switchFiles) + self.timer.start() def switchFiles(self): self.switchingLock.acquire() @@ -197,20 +196,16 @@ class AudioChopper(threading.Thread, metaclass=ABCMeta): decoder.kill() os.unlink(job.file) - def run(self) -> None: - logger.debug("WSJT chopper starting up") + def start(self): (self.wavefilename, self.wavefile) = self.getWaveFile() self._scheduleNextSwitch() - while self.doRun: - data = self.source.read(256) - if data is None or (isinstance(data, bytes) and len(data) == 0): - self.doRun = False - else: - self.switchingLock.acquire() - self.wavefile.writeframes(data) - self.switchingLock.release() - logger.debug("WSJT chopper shutting down") + def write(self, data): + self.switchingLock.acquire() + self.wavefile.writeframes(data) + self.switchingLock.release() + + def stop(self): self.outputReader.close() self.outputWriter.close() self.cancelTimer() @@ -219,8 +214,33 @@ class AudioChopper(threading.Thread, metaclass=ABCMeta): except Exception: logger.exception("error removing undecoded file") + +class AudioChopper(threading.Thread, metaclass=ABCMeta): + def __init__(self, dsp, source, *profiles: AudioChopperProfile): + self.source = source + self.writers = [AudioWriter(dsp, source, p) for p in profiles] + self.doRun = True + super().__init__() + + def run(self) -> None: + logger.debug("Audio chopper starting up") + for w in self.writers: + w.start() + while self.doRun: + data = self.source.read(256) + if data is None or (isinstance(data, bytes) and len(data) == 0): + self.doRun = False + else: + for w in self.writers: + w.write(data) + + logger.debug("Audio chopper shutting down") + for w in self.writers: + w.stop() + def read(self): try: - return self.outputReader.recv() + readers = wait([w.outputReader for w in self.writers]) + return [r.recv() for r in readers] except EOFError: return None diff --git a/owrx/js8.py b/owrx/js8.py index 303a273..5611f07 100644 --- a/owrx/js8.py +++ b/owrx/js8.py @@ -26,40 +26,41 @@ class Js8NormalProfile(AudioChopperProfile): class Js8Parser(Parser): decoderRegex = re.compile(" ?") - def parse(self, raw): - try: - freq, raw_msg = raw - self.setDialFrequency(freq) - msg = raw_msg.decode().rstrip() - if Js8Parser.decoderRegex.match(msg): - return - if msg.startswith(" EOF on input file"): - return + def parse(self, messages): + for raw in messages: + try: + freq, raw_msg = raw + self.setDialFrequency(freq) + msg = raw_msg.decode().rstrip() + if Js8Parser.decoderRegex.match(msg): + return + if msg.startswith(" EOF on input file"): + return - logger.debug(msg) + logger.debug(msg) - frame = Js8().parse_message(msg) - self.handler.write_js8_message(frame, self.dial_freq) - logger.debug(frame) + frame = Js8().parse_message(msg) + self.handler.write_js8_message(frame, self.dial_freq) + logger.debug(frame) - self.pushDecode() + self.pushDecode() - if (isinstance(frame, Js8FrameHeartbeat) or isinstance(frame, Js8FrameCompound)) and frame.grid: - Map.getSharedInstance().updateLocation( - frame.callsign, LocatorLocation(frame.grid), "JS8", self.band - ) - PskReporter.getSharedInstance().spot({ - "callsign": frame.callsign, - "mode": "JS8", - "locator": frame.grid, - "freq": self.dial_freq + frame.freq, - "db": frame.db, - "timestamp": frame.timestamp, - "msg": str(frame) - }) + if (isinstance(frame, Js8FrameHeartbeat) or isinstance(frame, Js8FrameCompound)) and frame.grid: + Map.getSharedInstance().updateLocation( + frame.callsign, LocatorLocation(frame.grid), "JS8", self.band + ) + PskReporter.getSharedInstance().spot({ + "callsign": frame.callsign, + "mode": "JS8", + "locator": frame.grid, + "freq": self.dial_freq + frame.freq, + "db": frame.db, + "timestamp": frame.timestamp, + "msg": str(frame) + }) - except Exception: - logger.exception("error while parsing js8 message") + except Exception: + logger.exception("error while parsing js8 message") def pushDecode(self): metrics = Metrics.getSharedInstance() diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 475e3b3..6e4ea1a 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -10,7 +10,6 @@ from abc import ABC, abstractmethod import logging logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) class Ft8Profile(AudioChopperProfile): @@ -75,34 +74,35 @@ class Ft4Profile(AudioChopperProfile): class WsjtParser(Parser): modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"} - def parse(self, data): - try: - freq, raw_msg = data - self.setDialFrequency(freq) - msg = raw_msg.decode().rstrip() - # known debug messages we know to skip - if msg.startswith(""): - return - if msg.startswith(" EOF on input file"): - return + def parse(self, messages): + for data in messages: + try: + freq, raw_msg = data + self.setDialFrequency(freq) + msg = raw_msg.decode().rstrip() + # known debug messages we know to skip + if msg.startswith(""): + return + if msg.startswith(" EOF on input file"): + return - modes = list(WsjtParser.modes.keys()) - if msg[21] in modes or msg[19] in modes: - decoder = Jt9Decoder() - else: - decoder = WsprDecoder() - out = decoder.parse(msg, freq) - if "mode" in out: - self.pushDecode(out["mode"]) - if "callsign" in out and "locator" in out: - Map.getSharedInstance().updateLocation( - out["callsign"], LocatorLocation(out["locator"]), out["mode"], self.band - ) - PskReporter.getSharedInstance().spot(out) + modes = list(WsjtParser.modes.keys()) + if msg[21] in modes or msg[19] in modes: + decoder = Jt9Decoder() + else: + decoder = WsprDecoder() + out = decoder.parse(msg, freq) + if "mode" in out: + self.pushDecode(out["mode"]) + if "callsign" in out and "locator" in out: + Map.getSharedInstance().updateLocation( + out["callsign"], LocatorLocation(out["locator"]), out["mode"], self.band + ) + PskReporter.getSharedInstance().spot(out) - self.handler.write_wsjt_message(out) - except ValueError: - logger.exception("error while parsing wsjt message") + self.handler.write_wsjt_message(out) + except ValueError: + logger.exception("error while parsing wsjt message") def pushDecode(self, mode): metrics = Metrics.getSharedInstance() From 2df56ad8b97bf7801845b56435413696c62e415e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 23 Apr 2020 00:34:49 +0200 Subject: [PATCH 229/475] js8 slow mode (attempt?) --- owrx/audio.py | 4 ++-- owrx/js8.py | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/owrx/audio.py b/owrx/audio.py index 4e81f06..0b1cbc4 100644 --- a/owrx/audio.py +++ b/owrx/audio.py @@ -13,7 +13,7 @@ from queue import Queue, Full import logging logger = logging.getLogger(__name__) -#logger.setLevel(logging.INFO) +logger.setLevel(logging.INFO) class QueueJob(object): @@ -132,7 +132,7 @@ class AudioWriter(object): def getWaveFile(self): filename = "{tmp_dir}/openwebrx-audiochopper-{id}-{timestamp}.wav".format( tmp_dir=self.tmp_dir, - id=id(self.profile), + id=id(self), timestamp=datetime.utcnow().strftime(self.profile.getFileTimestampFormat()), ) wavefile = wave.open(filename, "wb") diff --git a/owrx/js8.py b/owrx/js8.py index 5611f07..80b69ea 100644 --- a/owrx/js8.py +++ b/owrx/js8.py @@ -6,16 +6,14 @@ from js8py.frames import Js8FrameHeartbeat, Js8FrameCompound from owrx.map import Map, LocatorLocation from owrx.pskreporter import PskReporter from owrx.metrics import Metrics, CounterMetric +from abc import ABCMeta import logging logger = logging.getLogger(__name__) -class Js8NormalProfile(AudioChopperProfile): - def getInterval(self): - return 15 - +class Js8Profile(AudioChopperProfile, metaclass=ABCMeta): def getFileTimestampFormat(self): return "%y%m%d_%H%M%S" @@ -23,6 +21,16 @@ class Js8NormalProfile(AudioChopperProfile): return ["js8", "--js8", "-d", str(self.decoding_depth("js8")), file] +class Js8NormalProfile(Js8Profile): + def getInterval(self): + return 15 + + +class Js8SlowProfile(Js8Profile): + def getInterval(self): + return 30 + + class Js8Parser(Parser): decoderRegex = re.compile(" ?") From 280e39d9c4960b28b1ffde8af4d2fee02e7fa208 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 23 Apr 2020 22:19:07 +0200 Subject: [PATCH 230/475] js8 slow mode --- owrx/js8.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/owrx/js8.py b/owrx/js8.py index 80b69ea..bc66626 100644 --- a/owrx/js8.py +++ b/owrx/js8.py @@ -6,7 +6,7 @@ from js8py.frames import Js8FrameHeartbeat, Js8FrameCompound from owrx.map import Map, LocatorLocation from owrx.pskreporter import PskReporter from owrx.metrics import Metrics, CounterMetric -from abc import ABCMeta +from abc import ABCMeta, abstractmethod import logging @@ -18,18 +18,28 @@ class Js8Profile(AudioChopperProfile, metaclass=ABCMeta): return "%y%m%d_%H%M%S" def decoder_commandline(self, file): - return ["js8", "--js8", "-d", str(self.decoding_depth("js8")), file] + return ["js8", "--js8", "-b", self.get_sub_mode(), "-d", str(self.decoding_depth("js8")), file] + + @abstractmethod + def get_sub_mode(self): + pass class Js8NormalProfile(Js8Profile): def getInterval(self): return 15 + def get_sub_mode(self): + return "A" + class Js8SlowProfile(Js8Profile): def getInterval(self): return 30 + def get_sub_mode(self): + return "E" + class Js8Parser(Parser): decoderRegex = re.compile(" ?") From 34838abfa9fba55bec2954e300aaf2da885b9fb8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 23 Apr 2020 22:21:26 +0200 Subject: [PATCH 231/475] profiles --- csdr/csdr.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/csdr/csdr.py b/csdr/csdr.py index 07feb01..cb994a4 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -30,7 +30,7 @@ from functools import partial from owrx.kiss import KissClient, DirewolfConfig from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile -from owrx.js8 import Js8NormalProfile +from owrx.js8 import Js8NormalProfile, Js8SlowProfile from owrx.audio import AudioChopper import logging @@ -451,23 +451,23 @@ class dsp(object): if self.isWsjtMode(): smd = self.get_secondary_demodulator() - chopper_profile = None + chopper_profiles = None output_name = "wsjt_demod" if smd == "ft8": - chopper_profile = Ft8Profile() + chopper_profiles = [Ft8Profile()] elif smd == "wspr": - chopper_profile = WsprProfile() + chopper_profiles = [WsprProfile()] elif smd == "jt65": - chopper_profile = Jt65Profile() + chopper_profiles = [Jt65Profile()] elif smd == "jt9": - chopper_profile = Jt9Profile() + chopper_profiles = [Jt9Profile()] elif smd == "ft4": - chopper_profile = Ft4Profile() + chopper_profiles = [Ft4Profile()] elif smd == "js8": - chopper_profile = Js8NormalProfile() + chopper_profiles = [Js8NormalProfile(), Js8SlowProfile()] output_name = "js8_demod" - if chopper_profile is not None: - chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile) + if chopper_profiles is not None: + chopper = AudioChopper(self, self.secondary_process_demod.stdout, *chopper_profiles) chopper.start() self.output.send_output(output_name, chopper.read) elif self.isPacket(): From 623f21f769031359e59c25a0d7e42e5e9b65f9e3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 23 Apr 2020 22:27:03 +0200 Subject: [PATCH 232/475] fast and turbo modes --- csdr/csdr.py | 4 ++-- owrx/js8.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/csdr/csdr.py b/csdr/csdr.py index cb994a4..611ab19 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -30,7 +30,7 @@ from functools import partial from owrx.kiss import KissClient, DirewolfConfig from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile -from owrx.js8 import Js8NormalProfile, Js8SlowProfile +from owrx.js8 import Js8NormalProfile, Js8SlowProfile, Js8FastProfile, Js8TurboProfile from owrx.audio import AudioChopper import logging @@ -464,7 +464,7 @@ class dsp(object): elif smd == "ft4": chopper_profiles = [Ft4Profile()] elif smd == "js8": - chopper_profiles = [Js8NormalProfile(), Js8SlowProfile()] + chopper_profiles = [Js8NormalProfile(), Js8SlowProfile(), Js8FastProfile(), Js8TurboProfile()] output_name = "js8_demod" if chopper_profiles is not None: chopper = AudioChopper(self, self.secondary_process_demod.stdout, *chopper_profiles) diff --git a/owrx/js8.py b/owrx/js8.py index bc66626..859c1d0 100644 --- a/owrx/js8.py +++ b/owrx/js8.py @@ -41,6 +41,22 @@ class Js8SlowProfile(Js8Profile): return "E" +class Js8FastProfile(Js8Profile): + def getInterval(self): + return 10 + + def get_sub_mode(self): + return "B" + + +class Js8TurboProfile(Js8Profile): + def getInterval(self): + return 6 + + def get_sub_mode(self): + return "C" + + class Js8Parser(Parser): decoderRegex = re.compile(" ?") From 4e67be8a3c92ccffe77ac8d31ac341de62da6123 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 23 Apr 2020 23:30:56 +0200 Subject: [PATCH 233/475] dynamic profiles --- config_webrx.py | 3 +++ csdr/csdr.py | 4 ++-- owrx/js8.py | 20 +++++++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index f9ad9bc..1829211 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -301,6 +301,9 @@ wsjt_decoding_depth = 3 # jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent wsjt_decoding_depths = {"jt65": 1} +# JS8 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled. +js8_enabled_profiles = ["normal", "slow"] + temporary_directory = "/tmp" services_enabled = False diff --git a/csdr/csdr.py b/csdr/csdr.py index 611ab19..76c4876 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -30,7 +30,7 @@ from functools import partial from owrx.kiss import KissClient, DirewolfConfig from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile -from owrx.js8 import Js8NormalProfile, Js8SlowProfile, Js8FastProfile, Js8TurboProfile +from owrx.js8 import Js8Profiles from owrx.audio import AudioChopper import logging @@ -464,7 +464,7 @@ class dsp(object): elif smd == "ft4": chopper_profiles = [Ft4Profile()] elif smd == "js8": - chopper_profiles = [Js8NormalProfile(), Js8SlowProfile(), Js8FastProfile(), Js8TurboProfile()] + chopper_profiles = Js8Profiles.getEnabledProfiles() output_name = "js8_demod" if chopper_profiles is not None: chopper = AudioChopper(self, self.secondary_process_demod.stdout, *chopper_profiles) diff --git a/owrx/js8.py b/owrx/js8.py index 859c1d0..8b41ce2 100644 --- a/owrx/js8.py +++ b/owrx/js8.py @@ -3,9 +3,10 @@ from .parser import Parser import re from js8py import Js8 from js8py.frames import Js8FrameHeartbeat, Js8FrameCompound -from owrx.map import Map, LocatorLocation -from owrx.pskreporter import PskReporter -from owrx.metrics import Metrics, CounterMetric +from .map import Map, LocatorLocation +from .pskreporter import PskReporter +from .metrics import Metrics, CounterMetric +from .config import Config from abc import ABCMeta, abstractmethod import logging @@ -13,6 +14,19 @@ import logging logger = logging.getLogger(__name__) +class Js8Profiles(object): + @staticmethod + def getEnabledProfiles(): + config = Config.get() + profiles = config["js8_enabled_profiles"] if "js8_enabled_profiles" in config else [] + return [Js8Profiles.loadProfile(p) for p in profiles] + + @staticmethod + def loadProfile(profileName): + className = "Js8{0}Profile".format(profileName[0].upper() + profileName[1:].lower()) + return globals()[className]() + + class Js8Profile(AudioChopperProfile, metaclass=ABCMeta): def getFileTimestampFormat(self): return "%y%m%d_%H%M%S" From a828f61c72f9b025f26a6af00f9fb8ffa16ce3c4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 24 Apr 2020 23:47:05 +0200 Subject: [PATCH 234/475] use right message delay for mode --- htdocs/lib/Js8Threads.js | 32 ++++++++++++++++++++++++++++---- owrx/connection.py | 3 ++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/htdocs/lib/Js8Threads.js b/htdocs/lib/Js8Threads.js index d665b10..4cecf46 100644 --- a/htdocs/lib/Js8Threads.js +++ b/htdocs/lib/Js8Threads.js @@ -41,7 +41,7 @@ Js8Thread.prototype.renderMessages = function() { var msg = this.messages[i]; if (msg.thread_type & 1) { res.push('[ '); - } else if (i === 0 || msg.timestamp - this.messages[i - 1].timestamp > 15000) { + } else if (i === 0 || msg.timestamp - this.messages[i - 1].timestamp > this.getMessageDuration()) { res.push(' ... '); } res.push(msg.msg); @@ -54,6 +54,30 @@ Js8Thread.prototype.renderMessages = function() { return res.join(''); }; +Js8Thread.prototype.getMessageDuration = function() { + switch (this.getMode()) { + case 'A': + return 15000; + case 'E': + return 30000; + case 'B': + return 10000; + case 'C': + return 6000; + } +}; + +Js8Thread.prototype.getMode = function() { + // we filter messages by mode, so the first one is as good as any + if (!this.messages.length) return; + return this.messages[0].mode; +}; + +Js8Thread.prototype.acceptsMode = function(mode) { + var currentMode = this.getMode(); + return typeof(currentMode) === 'undefined' || currentMode === mode; +}; + Js8Thread.prototype.renderTimestamp = function(timestamp) { var t = new Date(timestamp); var pad = function (i) { @@ -91,10 +115,10 @@ Js8Threader.prototype.purgeOldMessages = function() { }); }; -Js8Threader.prototype.findThread = function(freq) { +Js8Threader.prototype.findThread = function(freq, mode) { var matching = this.threads.filter(function(thread) { // max frequency deviation: 5 Hz. this may be a little tight. - return thread.isOpen() && Math.abs(thread.getAverageFrequency() - freq) <= 5; + return thread.isOpen() && thread.acceptsMode(mode) && Math.abs(thread.getAverageFrequency() - freq) <= 5; }); matching.sort(function(a, b){ return b.getLatestTimestamp() - a.getLatestTimestamp(); @@ -106,7 +130,7 @@ Js8Threader.prototype.pushMessage = function(message) { var thread; // only look for exising threads if the message is not a starting message if ((message.thread_type & 1) === 0) { - thread = this.findThread(message.freq); + thread = this.findThread(message.freq, message.mode); } if (!thread) { var line = $(""); diff --git a/owrx/connection.py b/owrx/connection.py index 7432978..b483a22 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -341,7 +341,8 @@ class OpenWebRxReceiverClient(Client): "db": frame.db, "dt": frame.dt, "freq": freq + frame.freq, - "thread_type": frame.thread_type + "thread_type": frame.thread_type, + "mode": frame.mode }}) From 978eea400dbfd66f48444c1e8b78686d13b42bda Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Apr 2020 16:22:40 +0200 Subject: [PATCH 235/475] clean up wsjt remainders in absctract code --- config_webrx.py | 15 +++++++++------ owrx/audio.py | 25 +++++++------------------ owrx/controllers/settings.py | 4 ++-- owrx/js8.py | 8 ++++++++ owrx/wsjt.py | 26 ++++++++++++++++++++------ 5 files changed, 46 insertions(+), 32 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index 1829211..fc1fc12 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -285,16 +285,17 @@ google_maps_api_key = "" # in seconds; default: 2 hours map_position_retention_time = 2 * 60 * 60 -# wsjt decoder queue configuration -# due to the nature of the wsjt operating modes (ft8, ft8, jt9, jt65 and wspr), the data is recorded for a given amount -# of time (6.5 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads. +# decoder queue configuration +# due to the nature of some operating modes (ft8, ft8, jt9, jt65, wspr and js8), the data is recorded for a given amount +# of time (6 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads. # to mitigate this, the recordings will be queued and processed in sequence. # the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread) -wsjt_queue_workers = 2 +decoding_queue_workers = 2 # the maximum queue length will cause decodes to be dumped if the workers cannot keep up # if you are running background services, make sure this number is high enough to accept the task influx during peaks -# i.e. this should be higher than the number of wsjt services running at the same time -wsjt_queue_length = 10 +# i.e. this should be higher than the number of decoding services running at the same time +decoding_queue_length = 10 + # wsjt decoding depth will allow more results, but will also consume more cpu wsjt_decoding_depth = 3 # can also be set for each mode separately @@ -303,6 +304,8 @@ wsjt_decoding_depths = {"jt65": 1} # JS8 comes in different speeds: normal, slow, fast, turbo. This setting controls which ones are enabled. js8_enabled_profiles = ["normal", "slow"] +# JS8 decoding depth; higher value will get more results, but will also consume more cpu +js8_decoding_depth = 3 temporary_directory = "/tmp" diff --git a/owrx/audio.py b/owrx/audio.py index 0b1cbc4..aa8b5c0 100644 --- a/owrx/audio.py +++ b/owrx/audio.py @@ -52,21 +52,21 @@ class DecoderQueue(Queue): with DecoderQueue.creationLock: if DecoderQueue.sharedInstance is None: pm = Config.get() - DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["wsjt_queue_length"], workers=pm["wsjt_queue_workers"]) + DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"]) return DecoderQueue.sharedInstance def __init__(self, maxsize, workers): super().__init__(maxsize) metrics = Metrics.getSharedInstance() - metrics.addMetric("wsjt.queue.length", DirectMetric(self.qsize)) + metrics.addMetric("decoding.queue.length", DirectMetric(self.qsize)) self.inCounter = CounterMetric() - metrics.addMetric("wsjt.queue.in", self.inCounter) + metrics.addMetric("decoding.queue.in", self.inCounter) self.outCounter = CounterMetric() - metrics.addMetric("wsjt.queue.out", self.outCounter) + metrics.addMetric("decoding.queue.out", self.outCounter) self.overflowCounter = CounterMetric() - metrics.addMetric("wsjt.queue.overflow", self.overflowCounter) + metrics.addMetric("decoding.queue.overflow", self.overflowCounter) self.errorCounter = CounterMetric() - metrics.addMetric("wsjt.queue.error", self.errorCounter) + metrics.addMetric("decoding.queue.error", self.errorCounter) self.workers = [self.newWorker() for _ in range(0, workers)] def put(self, item, **kwars): @@ -105,17 +105,6 @@ class AudioChopperProfile(ABC): def decoder_commandline(self, file): pass - def decoding_depth(self, mode): - pm = Config.get() - # mode-specific setting? - if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]: - return pm["wsjt_decoding_depths"][mode] - # return global default - if "wsjt_decoding_depth" in pm: - return pm["wsjt_decoding_depth"] - # default when no setting is provided - return 3 - class AudioWriter(object): def __init__(self, dsp, source, profile: AudioChopperProfile): @@ -173,7 +162,7 @@ class AudioWriter(object): try: DecoderQueue.getSharedInstance().put(QueueJob(self, filename, self.dsp.get_operating_freq())) except Full: - logger.warning("wsjt decoding queue overflow; dropping one file") + logger.warning("decoding queue overflow; dropping one file") os.unlink(filename) self._scheduleNextSwitch() diff --git a/owrx/controllers/settings.py b/owrx/controllers/settings.py index c5b6288..527253f 100644 --- a/owrx/controllers/settings.py +++ b/owrx/controllers/settings.py @@ -146,8 +146,8 @@ class SettingsController(AdminController): ), Section( "WSJT-X settings", - NumberInput("wsjt_queue_workers", "Number of WSJT decoding workers"), - NumberInput("wsjt_queue_length", "Maximum length of WSJT job queue"), + NumberInput("decoding_queue_workers", "Number of decoding workers"), + NumberInput("decoding_queue_length", "Maximum length of decoding job queue"), NumberInput( "wsjt_decoding_depth", "WSJT decoding depth", diff --git a/owrx/js8.py b/owrx/js8.py index 8b41ce2..c303d9d 100644 --- a/owrx/js8.py +++ b/owrx/js8.py @@ -28,6 +28,14 @@ class Js8Profiles(object): class Js8Profile(AudioChopperProfile, metaclass=ABCMeta): + def decoding_depth(self, mode): + pm = Config.get() + # return global default + if "js8_decoding_depth" in pm: + return pm["js8_decoding_depth"] + # default when no setting is provided + return 3 + def getFileTimestampFormat(self): return "%y%m%d_%H%M%S" diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 6e4ea1a..046ae7d 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -5,14 +5,28 @@ from owrx.metrics import Metrics, CounterMetric from owrx.pskreporter import PskReporter from owrx.parser import Parser from owrx.audio import AudioChopperProfile -from abc import ABC, abstractmethod +from abc import ABC, ABCMeta, abstractmethod +from owrx.config import Config import logging logger = logging.getLogger(__name__) -class Ft8Profile(AudioChopperProfile): +class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta): + def decoding_depth(self, mode): + pm = Config.get() + # mode-specific setting? + if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]: + return pm["wsjt_decoding_depths"][mode] + # return global default + if "wsjt_decoding_depth" in pm: + return pm["wsjt_decoding_depth"] + # default when no setting is provided + return 3 + + +class Ft8Profile(WsjtProfile): def getInterval(self): return 15 @@ -23,7 +37,7 @@ class Ft8Profile(AudioChopperProfile): return ["jt9", "--ft8", "-d", str(self.decoding_depth("ft8")), file] -class WsprProfile(AudioChopperProfile): +class WsprProfile(WsjtProfile): def getInterval(self): return 120 @@ -38,7 +52,7 @@ class WsprProfile(AudioChopperProfile): return cmd -class Jt65Profile(AudioChopperProfile): +class Jt65Profile(WsjtProfile): def getInterval(self): return 60 @@ -49,7 +63,7 @@ class Jt65Profile(AudioChopperProfile): return ["jt9", "--jt65", "-d", str(self.decoding_depth("jt65")), file] -class Jt9Profile(AudioChopperProfile): +class Jt9Profile(WsjtProfile): def getInterval(self): return 60 @@ -60,7 +74,7 @@ class Jt9Profile(AudioChopperProfile): return ["jt9", "--jt9", "-d", str(self.decoding_depth("jt9")), file] -class Ft4Profile(AudioChopperProfile): +class Ft4Profile(WsjtProfile): def getInterval(self): return 7.5 From 2198c00d00415b34e7a6e046f896eea481e96edd Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Apr 2020 17:33:30 +0200 Subject: [PATCH 236/475] add js8 settings to web configuration --- owrx/controllers/settings.py | 14 ++++++++++++-- owrx/form/__init__.py | 11 +++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/owrx/controllers/settings.py b/owrx/controllers/settings.py index 527253f..8243d09 100644 --- a/owrx/controllers/settings.py +++ b/owrx/controllers/settings.py @@ -11,6 +11,7 @@ from owrx.form import ( DropdownInput, Option, ServicesCheckboxInput, + Js8ProfileCheckboxInput, ) import logging @@ -145,14 +146,23 @@ class SettingsController(AdminController): ), ), Section( - "WSJT-X settings", + "Decoding settings", NumberInput("decoding_queue_workers", "Number of decoding workers"), NumberInput("decoding_queue_length", "Maximum length of decoding job queue"), NumberInput( "wsjt_decoding_depth", - "WSJT decoding depth", + "Default WSJT decoding depth", infotext="A higher decoding depth will allow more results, but will also consume more cpu", ), + NumberInput( + "js8_decoding_depth", + "Js8Call decoding depth", + infotext="A higher decoding depth will allow more results, but will also consume more cpu", + ), + Js8ProfileCheckboxInput( + "js8_enabled_profiles", + "Js8Call enabled modes" + ), ), Section( "Background decoding", diff --git a/owrx/form/__init__.py b/owrx/form/__init__.py index 2861683..fb7160e 100644 --- a/owrx/form/__init__.py +++ b/owrx/form/__init__.py @@ -201,6 +201,17 @@ class ServicesCheckboxInput(MultiCheckboxInput): super().__init__(id, label, services, infotext) +class Js8ProfileCheckboxInput(MultiCheckboxInput): + def __init__(self, id, label, infotext=None): + profiles = [ + Option("normal", "Normal (15s, 50Hz, ~16WPM)"), + Option("slow", "Slow (30s, 25Hz, ~8WPM"), + Option("fast", "Fast (10s, 80Hz, ~24WPM"), + Option("turbo", "Turbo (6s, 160Hz, ~40WPM"), + ] + super().__init__(id, label, profiles, infotext) + + class DropdownInput(Input): def __init__(self, id, label, options, infotext=None): super().__init__(id, label, infotext=infotext) From b58357741a743acdb73153e1111375181df6136f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Apr 2020 19:05:24 +0200 Subject: [PATCH 237/475] separate modes in here, too --- csdr/csdr.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/csdr/csdr.py b/csdr/csdr.py index 76c4876..8fe1606 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -338,7 +338,7 @@ class dsp(object): "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8", "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8", ] - elif self.isWsjtMode(which): + elif self.isWsjtMode(which) or self.isJs8(which): chain += ["csdr realpart_cf"] if self.last_decimation != 1.0: chain += ["csdr fractional_decimator_ff {last_decimation}"] @@ -451,25 +451,25 @@ class dsp(object): if self.isWsjtMode(): smd = self.get_secondary_demodulator() - chopper_profiles = None - output_name = "wsjt_demod" + chopper_profile = None if smd == "ft8": - chopper_profiles = [Ft8Profile()] + chopper_profile = Ft8Profile() elif smd == "wspr": - chopper_profiles = [WsprProfile()] + chopper_profile = WsprProfile() elif smd == "jt65": - chopper_profiles = [Jt65Profile()] + chopper_profile = Jt65Profile() elif smd == "jt9": - chopper_profiles = [Jt9Profile()] + chopper_profile = Jt9Profile() elif smd == "ft4": - chopper_profiles = [Ft4Profile()] - elif smd == "js8": - chopper_profiles = Js8Profiles.getEnabledProfiles() - output_name = "js8_demod" - if chopper_profiles is not None: - chopper = AudioChopper(self, self.secondary_process_demod.stdout, *chopper_profiles) + chopper_profile = Ft4Profile() + if chopper_profile is not None: + chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile) chopper.start() - self.output.send_output(output_name, chopper.read) + self.output.send_output("wsjt_demod", chopper.read) + elif self.isJs8(): + chopper = AudioChopper(self, self.secondary_process_demod.stdout, *Js8Profiles.getEnabledProfiles()) + chopper.start() + self.output.send_output("js8_demod", chopper.read) elif self.isPacket(): # we best get the ax25 packets from the kiss socket kiss = KissClient(self.direwolf_port) @@ -570,7 +570,7 @@ class dsp(object): def get_audio_rate(self): if self.isDigitalVoice() or self.isPacket() or self.isPocsag(): return 48000 - elif self.isWsjtMode(): + elif self.isWsjtMode() or self.isJs8(): return 12000 return self.get_output_rate() @@ -582,7 +582,12 @@ class dsp(object): def isWsjtMode(self, demodulator=None): if demodulator is None: demodulator = self.get_secondary_demodulator() - return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "js8"] + return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] + + def isJs8(self, demodulator = None): + if demodulator is None: + demodulator = self.get_secondary_demodulator() + return demodulator == "js8" def isPacket(self, demodulator=None): if demodulator is None: From e064352621187df00c8c6a1e202a69065d189eb6 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Apr 2020 19:18:30 +0200 Subject: [PATCH 238/475] finally, remove debugging --- owrx/js8.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/owrx/js8.py b/owrx/js8.py index c303d9d..7d3c474 100644 --- a/owrx/js8.py +++ b/owrx/js8.py @@ -93,11 +93,8 @@ class Js8Parser(Parser): if msg.startswith(" EOF on input file"): return - logger.debug(msg) - frame = Js8().parse_message(msg) self.handler.write_js8_message(frame, self.dial_freq) - logger.debug(frame) self.pushDecode() From 4a2b81c793fb2ca3fff49ed55327f8ad0b7cbbb2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Apr 2020 20:32:11 +0200 Subject: [PATCH 239/475] use autogain on airspyhf --- config_webrx.py | 6 +----- htdocs/gfx/openwebrx-ha5kfu-top-logo.png | Bin 2010 -> 0 bytes 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 htdocs/gfx/openwebrx-ha5kfu-top-logo.png diff --git a/config_webrx.py b/config_webrx.py index fc1fc12..27c4835 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -150,11 +150,11 @@ sdrs = { "name": "Airspy HF+", "type": "airspyhf", "ppm": 0, + "rf_gain": "auto", "profiles": { "20m": { "name": "20m", "center_freq": 14150000, - "rf_gain": 10, "samp_rate": 768000, "start_freq": 14070000, "start_mod": "usb", @@ -162,7 +162,6 @@ sdrs = { "30m": { "name": "30m", "center_freq": 10125000, - "rf_gain": 10, "samp_rate": 192000, "start_freq": 10142000, "start_mod": "usb", @@ -170,7 +169,6 @@ sdrs = { "40m": { "name": "40m", "center_freq": 7100000, - "rf_gain": 10, "samp_rate": 256000, "start_freq": 7070000, "start_mod": "usb", @@ -178,7 +176,6 @@ sdrs = { "80m": { "name": "80m", "center_freq": 3650000, - "rf_gain": 10, "samp_rate": 768000, "start_freq": 3570000, "start_mod": "usb", @@ -186,7 +183,6 @@ sdrs = { "49m": { "name": "49m Broadcast", "center_freq": 6000000, - "rf_gain": 10, "samp_rate": 768000, "start_freq": 6070000, "start_mod": "am", diff --git a/htdocs/gfx/openwebrx-ha5kfu-top-logo.png b/htdocs/gfx/openwebrx-ha5kfu-top-logo.png deleted file mode 100644 index 2686eef0aa16eceddfa68cc79dbcd626de4bec4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2010 zcmV<02POE4P)1xY1Elr{0Ss{cpV?dxXHc~-Ip$TbUD&7hY8@s4YJzJ{&h_nxRsGXF8 zc%XoUwv(2JptbMeVSe^{JhsO_n=JVxYi8~}_nhNvFA?GGu(4Ux?D)9r3EAN!VjqZiPh0T8`o_&!7RwA+3EdkI_&pe3 zVBp@cF<0{}03e_Y3AeeZxzpC`S|Wup01VqcBOt%#>70R;VWBes5L{xcr&;y+v)-d1 z+{l0h0V`McTF~n4ik*j+R22Cw$*wT-(a#P?aCalIHN zoiQdve$CTapaCQs;b8rr2eQ295dcVXTLk_UK>t=<)7!=UF93KUJ1mdzI@HIneiA1S~P(92_m|p z@?hpw5Y0w9B^WeU_Znd6Ah@jstuT(R*BaYujD8c8uX%Q~14fA%Yn~1PJscZ1O0)z2 zPYohcQ8C7g84MkaAB7W>U-R@_-D@ylf#8xvp|zkD(#DSFqUJV>n%ydDL_nDVD_8S4 zzvk&-KS>mrF*laJ*BH^xSXx>-FgrV&Q9Il1cKfNjR=x>las1WaJiWQOx$D%aQ~w%A zP*>cLD5$~qV0~j>mc`>tm3O)Uv4eTy)=$)I|Wey&t|iK+T7gy zl-fx|7Xh3EfOfn6TO!f{>={*v>OO9o)$VTSXh-d8n#=eL0p<932IiB}*}yQa?+*D~ zzIOScW^wMj{KW4r<2LjWBB zZD#%(0DuJ((VNVCXi|Z>`ujfylTH$a)|f?Fv#8nmHP2;%GeI1pN%h(Z=rYFN_uu^O z+8xD*!Gb|7Avp`+UVuAK-SyxvzV-5{Pfse4I`krd7bmTjgYmb9nbh@wE@OPS>!W+X z^Z>vB10Vvztd{7lL5xh()FGt~01(1;9LMdUF${Y#d|wwxqA8V1Ib!}s)Xlsm}boI4PgcEfD$7V zSPFd)jcJ-XF*jm-Ez>j$J(H%&OQ&BbZ=eo9G}$3tIiv{|KqF4y2PzLh%0C0>F$PPy z3ZffV|Pdzj#c8oE0&&)ioemj*)!8DAm`o?!lZsK6( zb*Eaj#}M1s2*msoDMYG-{&U`0DwOP&oZ_q!GiCH zlkn((zEN-mz!Sp^yB&rAV#YZ1-o;P8{29FhLaB$6QDf$`ThlTPBXw{sm&;iY9aNfL z2jIIFDI-1uT-E-rU8I7@1yOn61g4=;&{viMr&=93#>G!yWY$rl~y;qTp02+v0Xcg#du#$B%yuz#M?xvfchf z5@E)t6)P(%(?6bO-U^OHZhpfHkZS7n-}ZW@Md8iw6(Ww$$>qp20~ zjb)jJ!D%Y&ln661Fwv2CXQ21iH3k4RaDq9L#_D(M-Me=N{(<6TAesk9M=ox&sM)!i zXXR?1-zcM%A6~US1i+3}{8p({Iyxieg-$k`ky2)uS<7ZJKgFQa|5z=fAWT5Y!;Vab z;2eN20zk0)8nJq)%XRi#yGoJnCU$C2CnHcayjE=$HEJ$uu3E4$@Nob=MI15zu1)kV zcRH7dxha6xx_BqW7T+xqfO4PMW2s5QsOWKd!R}LjDvX|)&vyPR0s1Z&27nTnRzdjQ z4V#y*#Azo*+#RUOtM?O02X&6)xT%9T01)*}%bjY~b}E$ugeYLzm9@`v7;L9n{drFX zzDvpap48{qrOOTe4?ZTDtFhq@9smFU07*qoM6N<$f=qd`SpWb4 From 4dc10fb6a3779d9c729c00e0d803e5425c4f31ff Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Apr 2020 20:36:08 +0200 Subject: [PATCH 240/475] lose the logo --- htdocs/css/openwebrx-header.css | 8 +------- htdocs/include/header.include.html | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css index 884a049..c369fb1 100644 --- a/htdocs/css/openwebrx-header.css +++ b/htdocs/css/openwebrx-header.css @@ -47,12 +47,6 @@ float: left; } -#webrx-ha5kfu-top-logo -{ - float: right; - padding: 15px; -} - #webrx-rx-avatar { background-color: rgba(154, 154, 154, .5); @@ -149,7 +143,7 @@ #openwebrx-main-buttons { - padding: 5px 0; + padding: 5px 15px; display: flex; list-style: none; float: right; diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html index 3a4fb11..ac48bc1 100644 --- a/htdocs/include/header.include.html +++ b/htdocs/include/header.include.html @@ -3,7 +3,6 @@
-
From dd492fa63c6716cedd8e78993d0e1f6bcd3387d0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Apr 2020 20:52:41 +0200 Subject: [PATCH 241/475] hide "settings" link if features is disabled --- htdocs/include/header.include.html | 2 +- owrx/controllers/template.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html index ac48bc1..c86977c 100644 --- a/htdocs/include/header.include.html +++ b/htdocs/include/header.include.html @@ -17,7 +17,7 @@

Log

Receiver

Map
-
Settings
+ ${settingslink}
diff --git a/owrx/controllers/template.py b/owrx/controllers/template.py index 6746012..4adbe8c 100644 --- a/owrx/controllers/template.py +++ b/owrx/controllers/template.py @@ -1,6 +1,7 @@ from . import Controller import pkg_resources from string import Template +from owrx.config import Config class TemplateController(Controller): @@ -19,7 +20,11 @@ class TemplateController(Controller): class WebpageController(TemplateController): def template_variables(self): - header = self.render_template("include/header.include.html") + settingslink = "" + pm = Config.get() + if "webadmin_enabled" in pm and pm["webadmin_enabled"]: + settingslink = """
Settings
""" + header = self.render_template("include/header.include.html", settingslink=settingslink) return {"header": header} From a4ebf872632bddcfcdad4a61c470b71727f4541c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Apr 2020 20:55:33 +0200 Subject: [PATCH 242/475] check for key --- owrx/controllers/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/owrx/controllers/admin.py b/owrx/controllers/admin.py index 6aa2a4b..9ea8b24 100644 --- a/owrx/controllers/admin.py +++ b/owrx/controllers/admin.py @@ -18,7 +18,7 @@ class AdminController(WebpageController): def handle_request(self): config = Config.get() - if not config["webadmin_enabled"]: + if "webadmin_enabled" not in config or not config["webadmin_enabled"]: self.send_response("Web Admin is disabled", code=403) return if self.authentication.isAuthenticated(self.request): From 2f011ea2495ec511da7779a83d4ba8bfc5a4f606 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Apr 2020 20:57:12 +0200 Subject: [PATCH 243/475] add remark about web admin --- config_webrx.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config_webrx.py b/config_webrx.py index 27c4835..fdde08e 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -327,4 +327,6 @@ pskreporter_enabled = False pskreporter_callsign = "N0CALL" # === Web admin settings === -webadmin_enabled = False +# this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote +# changes to the receiver settings. enable for testing in controlled environment only. +# webadmin_enabled = False From b874583931c6041ae854f798fd20179c62fba9d0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Apr 2020 21:42:00 +0200 Subject: [PATCH 244/475] setup for multiple settings sections --- htdocs/{admin.html => generalsettings.html} | 0 htdocs/settings.html | 26 +++++++++++++++++++++ owrx/controllers/session.py | 4 ++-- owrx/controllers/settings.py | 11 ++++++--- owrx/controllers/template.py | 2 +- owrx/http.py | 7 +++--- 6 files changed, 41 insertions(+), 9 deletions(-) rename htdocs/{admin.html => generalsettings.html} (100%) create mode 100644 htdocs/settings.html diff --git a/htdocs/admin.html b/htdocs/generalsettings.html similarity index 100% rename from htdocs/admin.html rename to htdocs/generalsettings.html diff --git a/htdocs/settings.html b/htdocs/settings.html new file mode 100644 index 0000000..659b660 --- /dev/null +++ b/htdocs/settings.html @@ -0,0 +1,26 @@ + + + + OpenWebRX Settings + + + + + + + + + +${header} +
+
+

Settings

+
+ + +
+ \ No newline at end of file diff --git a/owrx/controllers/session.py b/owrx/controllers/session.py index dd6d0f9..51bf399 100644 --- a/owrx/controllers/session.py +++ b/owrx/controllers/session.py @@ -46,12 +46,12 @@ class SessionController(WebpageController): if data["user"] in userlist: user = userlist[data["user"]] if user.password.is_valid(data["password"]): - # TODO pass the final destination # TODO evaluate password force_change and redirect to password change key = SessionStorage.getSharedInstance().startSession({"user": user.name}) cookie = SimpleCookie() cookie["owrx-session"] = key - self.send_redirect("/admin", cookies=cookie) + # TODO pass the final destination + self.send_redirect("/settings", cookies=cookie) return self.send_redirect("/login") diff --git a/owrx/controllers/settings.py b/owrx/controllers/settings.py index 8243d09..8f5e11e 100644 --- a/owrx/controllers/settings.py +++ b/owrx/controllers/settings.py @@ -44,6 +44,11 @@ class Section(object): class SettingsController(AdminController): + def indexAction(self): + self.serve_template("settings.html", **self.template_variables()) + + +class GeneralSettingsController(AdminController): sections = [ Section( "General settings", @@ -222,7 +227,7 @@ class SettingsController(AdminController): ] def render_sections(self): - sections = "".join(section.render() for section in SettingsController.sections) + sections = "".join(section.render() for section in GeneralSettingsController.sections) return """
{sections} @@ -235,7 +240,7 @@ class SettingsController(AdminController): ) def indexAction(self): - self.serve_template("admin.html", **self.template_variables()) + self.serve_template("generalsettings.html", **self.template_variables()) def template_variables(self): variables = super().template_variables() @@ -245,7 +250,7 @@ class SettingsController(AdminController): def processFormData(self): data = parse_qs(self.get_body().decode("utf-8")) data = { - k: v for i in SettingsController.sections for k, v in i.parse(data).items() + k: v for i in GeneralSettingsController.sections for k, v in i.parse(data).items() } config = Config.get() for k, v in data.items(): diff --git a/owrx/controllers/template.py b/owrx/controllers/template.py index 4adbe8c..6030fd1 100644 --- a/owrx/controllers/template.py +++ b/owrx/controllers/template.py @@ -23,7 +23,7 @@ class WebpageController(TemplateController): settingslink = "" pm = Config.get() if "webadmin_enabled" in pm and pm["webadmin_enabled"]: - settingslink = """
Settings
""" + settingslink = """
Settings
""" header = self.render_template("include/header.include.html", settingslink=settingslink) return {"header": header} diff --git a/owrx/http.py b/owrx/http.py index 7e8d104..b8ec63c 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -11,7 +11,7 @@ from owrx.controllers.assets import ( from owrx.controllers.websocket import WebSocketController from owrx.controllers.api import ApiController from owrx.controllers.metrics import MetricsController -from owrx.controllers.settings import SettingsController +from owrx.controllers.settings import SettingsController, GeneralSettingsController from owrx.controllers.session import SessionController from http.server import BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs @@ -100,8 +100,9 @@ class Router(object): StaticRoute("/features", FeatureController), StaticRoute("/api/features", ApiController), StaticRoute("/metrics", MetricsController), - StaticRoute("/admin", SettingsController), - StaticRoute("/admin", SettingsController, method="POST", options={"action": "processFormData"}), + StaticRoute("/settings", SettingsController), + StaticRoute("/generalsettings", GeneralSettingsController), + StaticRoute("/generalsettings", GeneralSettingsController, method="POST", options={"action": "processFormData"}), StaticRoute("/login", SessionController, options={"action": "loginAction"}), StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}), StaticRoute("/logout", SessionController, options={"action": "logoutAction"}), From 9942b3baf2a90a103f7330be59cd12a3e719100d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 25 Apr 2020 21:55:52 +0200 Subject: [PATCH 245/475] separate page for sdr settings --- htdocs/generalsettings.html | 2 +- htdocs/sdrsettings.html | 20 ++++++++++++++++++++ owrx/controllers/settings.py | 5 +++++ owrx/http.py | 3 ++- 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 htdocs/sdrsettings.html diff --git a/htdocs/generalsettings.html b/htdocs/generalsettings.html index 54e8fe4..edd52e7 100644 --- a/htdocs/generalsettings.html +++ b/htdocs/generalsettings.html @@ -14,7 +14,7 @@ ${header}
-

Settings

+

General settings

${sections}
diff --git a/htdocs/sdrsettings.html b/htdocs/sdrsettings.html new file mode 100644 index 0000000..0cb8ec4 --- /dev/null +++ b/htdocs/sdrsettings.html @@ -0,0 +1,20 @@ + + + + OpenWebRX Settings + + + + + + + + + +${header} +
+
+

SDR device settings

+
+
+ \ No newline at end of file diff --git a/owrx/controllers/settings.py b/owrx/controllers/settings.py index 8f5e11e..6934bf7 100644 --- a/owrx/controllers/settings.py +++ b/owrx/controllers/settings.py @@ -48,6 +48,11 @@ class SettingsController(AdminController): self.serve_template("settings.html", **self.template_variables()) +class SdrSettingsController(AdminController): + def indexAction(self): + self.serve_template("sdrsettings.html", **self.template_variables()) + + class GeneralSettingsController(AdminController): sections = [ Section( diff --git a/owrx/http.py b/owrx/http.py index b8ec63c..f7203a5 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -11,7 +11,7 @@ from owrx.controllers.assets import ( from owrx.controllers.websocket import WebSocketController from owrx.controllers.api import ApiController from owrx.controllers.metrics import MetricsController -from owrx.controllers.settings import SettingsController, GeneralSettingsController +from owrx.controllers.settings import SettingsController, GeneralSettingsController, SdrSettingsController from owrx.controllers.session import SessionController from http.server import BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs @@ -103,6 +103,7 @@ class Router(object): StaticRoute("/settings", SettingsController), StaticRoute("/generalsettings", GeneralSettingsController), StaticRoute("/generalsettings", GeneralSettingsController, method="POST", options={"action": "processFormData"}), + StaticRoute("/sdrsettings", SdrSettingsController), StaticRoute("/login", SessionController, options={"action": "loginAction"}), StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}), StaticRoute("/logout", SessionController, options={"action": "logoutAction"}), From 5282b5f8dffe1ef6447e95f9e65d5174a5ddf844 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 26 Apr 2020 01:54:48 +0200 Subject: [PATCH 246/475] implement redirect on login --- owrx/controllers/admin.py | 8 +++++++- owrx/controllers/session.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/owrx/controllers/admin.py b/owrx/controllers/admin.py index 9ea8b24..3879141 100644 --- a/owrx/controllers/admin.py +++ b/owrx/controllers/admin.py @@ -1,6 +1,11 @@ from .template import WebpageController from .session import SessionStorage from owrx.config import Config +from urllib import parse + +import logging + +logger = logging.getLogger(__name__) class Authentication(object): @@ -24,4 +29,5 @@ class AdminController(WebpageController): if self.authentication.isAuthenticated(self.request): super().handle_request() else: - self.send_redirect("/login") + target = "/login?{0}".format(parse.urlencode({"ref": self.request.path})) + self.send_redirect(target) diff --git a/owrx/controllers/session.py b/owrx/controllers/session.py index 51bf399..ac38a43 100644 --- a/owrx/controllers/session.py +++ b/owrx/controllers/session.py @@ -50,8 +50,8 @@ class SessionController(WebpageController): key = SessionStorage.getSharedInstance().startSession({"user": user.name}) cookie = SimpleCookie() cookie["owrx-session"] = key - # TODO pass the final destination - self.send_redirect("/settings", cookies=cookie) + target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings" + self.send_redirect(target, cookies=cookie) return self.send_redirect("/login") From fb90a4e54bacff5160be34ea06a2146a99214f1f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 26 Apr 2020 02:15:19 +0200 Subject: [PATCH 247/475] display sdr devices --- htdocs/css/admin.css | 4 ++++ htdocs/sdrsettings.html | 3 +++ owrx/controllers/settings.py | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/htdocs/css/admin.css b/htdocs/css/admin.css index 8836244..fa2b020 100644 --- a/htdocs/css/admin.css +++ b/htdocs/css/admin.css @@ -18,3 +18,7 @@ body { .row .map-input { margin: 15px 15px 0; } + +.device { + margin-top: 20px; +} diff --git a/htdocs/sdrsettings.html b/htdocs/sdrsettings.html index 0cb8ec4..51de0f0 100644 --- a/htdocs/sdrsettings.html +++ b/htdocs/sdrsettings.html @@ -16,5 +16,8 @@ ${header}

SDR device settings

+
+ ${devices} +
\ No newline at end of file diff --git a/owrx/controllers/settings.py b/owrx/controllers/settings.py index 6934bf7..7a219e3 100644 --- a/owrx/controllers/settings.py +++ b/owrx/controllers/settings.py @@ -49,6 +49,25 @@ class SettingsController(AdminController): class SdrSettingsController(AdminController): + def template_variables(self): + variables = super().template_variables() + variables["devices"] = self.render_devices() + return variables + + def render_devices(self): + def render_devicde(device_id, config): + return """ +
+
+ {device_name} +
+
+ device settings go here +
+
+ """.format(device_name=config["name"]) + return "".join(render_devicde(key, value) for key, value in Config.get()["sdrs"].items()) + def indexAction(self): self.serve_template("sdrsettings.html", **self.template_variables()) From e61d3a22a3de2c1d661df79ee328376d6d55a35f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 26 Apr 2020 13:49:03 +0200 Subject: [PATCH 248/475] add if_mode mapping for sdrplay, refs #105 --- owrx/source/sdrplay.py | 1 + 1 file changed, 1 insertion(+) diff --git a/owrx/source/sdrplay.py b/owrx/source/sdrplay.py index 9306b86..da2398a 100644 --- a/owrx/source/sdrplay.py +++ b/owrx/source/sdrplay.py @@ -9,6 +9,7 @@ class SdrplaySource(SoapyConnectorSource): "bias_tee": "biasT_ctrl", "rf_notch": "rfnotch_ctrl", "dab_notch": "dabnotch_ctrl", + "if_mode": "if_mode", } ) return mappings From 907787cfdc1ae6fce5293f3cd6db01bf9a956a54 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 26 Apr 2020 15:17:03 +0200 Subject: [PATCH 249/475] implement first stages of active mode communication --- htdocs/index.html | 1 + htdocs/lib/Modes.js | 28 ++++++++++++++++++++++++++ htdocs/openwebrx.js | 9 +++++++++ owrx/connection.py | 11 ++++++++++ owrx/form/__init__.py | 4 ++-- owrx/modes.py | 43 ++++++++++++++++++++++++++++++++++++++++ owrx/service/__init__.py | 30 ++-------------------------- 7 files changed, 96 insertions(+), 30 deletions(-) create mode 100644 htdocs/lib/Modes.js create mode 100644 owrx/modes.py diff --git a/htdocs/index.html b/htdocs/index.html index 77b0fe3..9027ef1 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -33,6 +33,7 @@ + diff --git a/htdocs/lib/Modes.js b/htdocs/lib/Modes.js new file mode 100644 index 0000000..ea7e8b7 --- /dev/null +++ b/htdocs/lib/Modes.js @@ -0,0 +1,28 @@ +var Modes = { + modes: [], + features: {}, + setModes:function(json){ + this.modes = json.map(function(m){ return new Mode(m); }); + }, + setFeatures:function(features){ + this.features = features; + }, + findByModulation:function(modulation){ + matches = this.modes.filter(function(m) { return m.modulation === modulation; }); + if (matches.length) return matches[0] + } +} + +var Mode = function(json){ + this.modulation = json.modulation; + this.name = json.name; + this.requirements = json.requirements; +}; + +Mode.prototype.isAvailable = function(){ + return this.requirements.map(function(r){ + return Modes.features[r]; + }).reduce(function(a, b){ + return a && b; + }, true); +} \ No newline at end of file diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 05d4f51..41c91a4 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1177,6 +1177,10 @@ function on_ws_recv(evt) { // set a higher reconnection timeout right away to avoid additional load reconnect_timeout = 16000; break; + case 'modes': + Modes.setModes(json['value']); + console.info(Modes); + break; default: console.warn('received message of unknown type: ' + json['type']); } @@ -2014,6 +2018,11 @@ function demodulator_digital_replace_last() { function demodulator_digital_replace(subtype) { if (secondary_demod === subtype) return; + var mode = Modes.findByModulation(subtype); + if (mode && !mode.isAvailable()) { + divlog('Digital mode "' + mode.name + '" not supported. Please check requirements', true); + return; + } switch (subtype) { case "bpsk31": case "bpsk63": diff --git a/owrx/connection.py b/owrx/connection.py index b483a22..4367e55 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -11,6 +11,7 @@ from owrx.bookmarks import Bookmarks from owrx.map import Map from owrx.locator import Locator from owrx.property import PropertyStack +from owrx.modes import Modes from multiprocessing import Queue from queue import Full from js8py import Js8Frame @@ -122,6 +123,9 @@ class OpenWebRxReceiverClient(Client): features = FeatureDetector().feature_availability() self.write_features(features) + modes = Modes.getModes() + self.write_modes(modes) + CpuUsageThread.getSharedInstance().add_client(self) def __sendProfiles(self): @@ -345,6 +349,13 @@ class OpenWebRxReceiverClient(Client): "mode": frame.mode }}) + def write_modes(self, modes): + self.send({"type": "modes", "value": [{ + "modulation": m.modulation, + "name": m.name, + "requirements": m.requirements + } for m in modes]}) + class MapConnection(Client): def __init__(self, conn): diff --git a/owrx/form/__init__.py b/owrx/form/__init__.py index fb7160e..7f59bf5 100644 --- a/owrx/form/__init__.py +++ b/owrx/form/__init__.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from owrx.service import ServiceDetector +from owrx.modes import Modes from owrx.config import Config @@ -196,7 +196,7 @@ class MultiCheckboxInput(Input): class ServicesCheckboxInput(MultiCheckboxInput): def __init__(self, id, label, infotext=None): services = [ - Option(s, s.upper()) for s in ServiceDetector.getAvailableServices() + Option(s.modulation, s.name) for s in Modes.getAvailableServices() ] super().__init__(id, label, services, infotext) diff --git a/owrx/modes.py b/owrx/modes.py new file mode 100644 index 0000000..b0721fc --- /dev/null +++ b/owrx/modes.py @@ -0,0 +1,43 @@ +from owrx.feature import FeatureDetector +from functools import reduce + + +class Mode(object): + def __init__(self, modulation, name, requirements=None, service=False): + self.modulation = modulation + self.name = name + self.requirements = requirements if requirements is not None else [] + self.service = service + + def is_available(self): + fd = FeatureDetector() + return reduce( + lambda a, b: a and b, [fd.is_available(r) for r in self.requirements], True + ) + + def is_service(self): + return self.service + + +class Modes(object): + mappings = [ + Mode("ft8", "FT8", ["wsjt-x"], True), + Mode("ft4", "FT4", ["wsjt-x"], True), + Mode("jt65", "JT65", ["wsjt-x"], True), + Mode("jt9", "JT9", ["wsjt-x"], True), + Mode("wspr", "WSPR", ["wsjt-x"], True), + Mode("packet", "Packet", ["packet"], True), + Mode("js8", "JS8Call", ["js8call"], True), + ] + + @staticmethod + def getModes(): + return Modes.mappings + + @staticmethod + def getAvailableModes(): + return [m for m in Modes.getModes() if m.is_available()] + + @staticmethod + def getAvailableServices(): + return [m for m in Modes.getAvailableModes() if m.is_service()] diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py index 8310fa4..d4c97df 100644 --- a/owrx/service/__init__.py +++ b/owrx/service/__init__.py @@ -8,12 +8,11 @@ from owrx.aprs import AprsParser from owrx.js8 import Js8Parser from owrx.config import Config from owrx.source.resampler import Resampler -from owrx.feature import FeatureDetector from owrx.property import PropertyLayer from js8py import Js8Frame from abc import ABCMeta, abstractmethod from .schedule import ServiceScheduler -from functools import reduce +from owrx.modes import Modes import logging @@ -60,31 +59,6 @@ class Js8ServiceOutput(ServiceOutput): return t == "js8_demod" -class ServiceDetector(object): - requirements = { - "ft8": ["wsjt-x"], - "ft4": ["wsjt-x"], - "jt65": ["wsjt-x"], - "jt9": ["wsjt-x"], - "wspr": ["wsjt-x"], - "packet": ["packet"], - "js8": ["js8call"], - } - - @staticmethod - def getAvailableServices(): - # TODO this should be in a more central place (the frontend also needs this) - fd = FeatureDetector() - - return [ - name - for name, requirements in ServiceDetector.requirements.items() - if reduce( - lambda a, b: a and b, [fd.is_available(r) for r in requirements], True - ) - ] - - class ServiceHandler(object): def __init__(self, source): self.lock = threading.Lock() @@ -120,7 +94,7 @@ class ServiceHandler(object): def isSupported(self, mode): configured = Config.get()["services_decoders"] - available = ServiceDetector.getAvailableServices() + available = [m.modulation for m in Modes.getAvailableServices()] return mode in configured and mode in available def shutdown(self): From bb1b561c476837e39998f29f0eb197c5887b954c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 26 Apr 2020 16:58:31 +0200 Subject: [PATCH 250/475] fully-automatic mode panel generation --- htdocs/index.html | 43 +------------------------------------------ htdocs/lib/Modes.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ htdocs/openwebrx.js | 2 +- owrx/connection.py | 1 + owrx/modes.py | 28 ++++++++++++++++++++-------- 5 files changed, 68 insertions(+), 51 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 9027ef1..f6d08ac 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -166,48 +166,7 @@ -
-
FM
-
AM
-
LSB
-
USB
-
CW
-
-
- - - - -
-
-
DIG
- -
+
diff --git a/htdocs/lib/Modes.js b/htdocs/lib/Modes.js index ea7e8b7..dda89f2 100644 --- a/htdocs/lib/Modes.js +++ b/htdocs/lib/Modes.js @@ -3,19 +3,64 @@ var Modes = { features: {}, setModes:function(json){ this.modes = json.map(function(m){ return new Mode(m); }); + this.updateModePanel(); }, setFeatures:function(features){ this.features = features; + this.updateModePanel(); }, findByModulation:function(modulation){ matches = this.modes.filter(function(m) { return m.modulation === modulation; }); if (matches.length) return matches[0] + }, + updateModePanel:function() { + var available = this.modes.filter(function(m){ return m.isAvailable(); }); + var normalModes = available.filter(function(m){ return !m.digimode; }); + var digiModes = available.filter(function(m){ return m.digimode; }); + + var index = 0; + var arrayLength = normalModes.length; + var chunks = []; + + + for (index = 0; index < arrayLength; index += 5) { + chunks.push(normalModes.slice(index, index + 5)); + } + + var html = [] + + html.push.apply(html, chunks.map(function(chunk){ + return $( + '
' + + chunk.map(function(m){ + return '
' + + m.name + '
'; + }).join('') + + '
'); + })); + + html.push($( + '
' + + '
DIG
' + + '' + + '
' + )); + + $("#openwebrx-panel-receiver").find(".openwebrx-modes").html(html); } } var Mode = function(json){ this.modulation = json.modulation; this.name = json.name; + this.digimode = json.digimode; this.requirements = json.requirements; }; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 41c91a4..9798d19 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1123,6 +1123,7 @@ function on_ws_recv(evt) { break; case "features": var features = json['value']; + Modes.setFeatures(features); for (var feature in features) { if (features.hasOwnProperty(feature)) { $('[data-feature="' + feature + '"]')[features[feature] ? "show" : "hide"](); @@ -1179,7 +1180,6 @@ function on_ws_recv(evt) { break; case 'modes': Modes.setModes(json['value']); - console.info(Modes); break; default: console.warn('received message of unknown type: ' + json['type']); diff --git a/owrx/connection.py b/owrx/connection.py index 4367e55..3d54ee8 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -353,6 +353,7 @@ class OpenWebRxReceiverClient(Client): self.send({"type": "modes", "value": [{ "modulation": m.modulation, "name": m.name, + "digimode": m.digimode, "requirements": m.requirements } for m in modes]}) diff --git a/owrx/modes.py b/owrx/modes.py index b0721fc..d2d890a 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -3,9 +3,10 @@ from functools import reduce class Mode(object): - def __init__(self, modulation, name, requirements=None, service=False): + def __init__(self, modulation, name, requirements=None, service=False, digimode=False): self.modulation = modulation self.name = name + self.digimode = digimode self.requirements = requirements if requirements is not None else [] self.service = service @@ -21,13 +22,24 @@ class Mode(object): class Modes(object): mappings = [ - Mode("ft8", "FT8", ["wsjt-x"], True), - Mode("ft4", "FT4", ["wsjt-x"], True), - Mode("jt65", "JT65", ["wsjt-x"], True), - Mode("jt9", "JT9", ["wsjt-x"], True), - Mode("wspr", "WSPR", ["wsjt-x"], True), - Mode("packet", "Packet", ["packet"], True), - Mode("js8", "JS8Call", ["js8call"], True), + Mode("nfm", "FM"), + Mode("am", "AM"), + Mode("lsb", "LSB"), + Mode("usb", "USB"), + Mode("cw", "CW"), + Mode("dmr", "DMR", requirements=["digital_voice_digiham"]), + Mode("dstar", "DStar", requirements=["digital_voice_dsd"]), + Mode("nxdn", "NXDN", requirements=["digital_voice_dsd"]), + Mode("ysf", "YSF", requirements=["digital_voice_digiham"]), + Mode("bpsk31", "BPSK31", digimode=True), + Mode("bpsk63", "BPSK63", digimode=True), + Mode("ft8", "FT8", requirements=["wsjt-x"], service=True, digimode=True), + Mode("ft4", "FT4", requirements=["wsjt-x"], service=True, digimode=True), + Mode("jt65", "JT65", requirements=["wsjt-x"], service=True, digimode=True), + Mode("jt9", "JT9", requirements=["wsjt-x"], service=True, digimode=True), + Mode("wspr", "WSPR", requirements=["wsjt-x"], service=True, digimode=True), + Mode("packet", "Packet", ["packet"], service=True, digimode=True), + Mode("js8", "JS8Call", requirements=["js8call"], service=True, digimode=True), ] @staticmethod From 39f9d4c27369a01b557b967f226c41b55b43b8c1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 26 Apr 2020 17:18:48 +0200 Subject: [PATCH 251/475] streamline button generation --- htdocs/lib/Modes.js | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/htdocs/lib/Modes.js b/htdocs/lib/Modes.js index dda89f2..e7b2597 100644 --- a/htdocs/lib/Modes.js +++ b/htdocs/lib/Modes.js @@ -18,27 +18,30 @@ var Modes = { var normalModes = available.filter(function(m){ return !m.digimode; }); var digiModes = available.filter(function(m){ return m.digimode; }); - var index = 0; - var arrayLength = normalModes.length; - var chunks = []; - - - for (index = 0; index < arrayLength; index += 5) { - chunks.push(normalModes.slice(index, index + 5)); - } var html = [] - html.push.apply(html, chunks.map(function(chunk){ + var buttons = normalModes.map(function(m){ return $( - '
' + - chunk.map(function(m){ - return '
' + - m.name + '
'; - }).join('') + - '
'); + '
' + + m.name + '
' + ); + }); + + var index = 0; + var arrayLength = buttons.length; + var chunks = []; + + for (index = 0; index < arrayLength; index += 5) { + chunks.push(buttons.slice(index, index + 5)); + } + + html.push.apply(html, chunks.map(function(chunk){ + $line = $('
'); + $line.append.apply($line, chunk); + return $line })); html.push($( @@ -55,7 +58,7 @@ var Modes = { $("#openwebrx-panel-receiver").find(".openwebrx-modes").html(html); } -} +}; var Mode = function(json){ this.modulation = json.modulation; From 449b3b39860e90583aec95d89dbf64e5b8a14ba9 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 26 Apr 2020 17:19:05 +0200 Subject: [PATCH 252/475] features no longer used on this level --- htdocs/openwebrx.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 9798d19..cd08808 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1122,13 +1122,7 @@ function on_ws_recv(evt) { } break; case "features": - var features = json['value']; - Modes.setFeatures(features); - for (var feature in features) { - if (features.hasOwnProperty(feature)) { - $('[data-feature="' + feature + '"]')[features[feature] ? "show" : "hide"](); - } - } + Modes.setFeatures(json['value']); break; case "metadata": update_metadata(json['value']); From 26321ab68b3775e7646cb3ab9ef605e5947964b8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 26 Apr 2020 18:45:41 +0200 Subject: [PATCH 253/475] keep more parameters on the server side --- csdr/csdr.py | 2 + htdocs/css/openwebrx.css | 4 ++ htdocs/lib/Modes.js | 13 +++-- htdocs/openwebrx.js | 116 ++++++++++----------------------------- owrx/connection.py | 37 ++++++++----- owrx/modes.py | 57 ++++++++++++------- 6 files changed, 106 insertions(+), 123 deletions(-) diff --git a/csdr/csdr.py b/csdr/csdr.py index 8fe1606..57cd5cb 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -607,6 +607,8 @@ class dsp(object): self.restart() def set_demodulator(self, demodulator): + if demodulator in ["usb", "lsb", "cw"]: + demodulator = "ssb" if self.demodulator == demodulator: return self.demodulator = demodulator diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index b5693c8..731bac1 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -446,6 +446,10 @@ input[type=range]:focus::-ms-fill-upper margin-right: 5px; } +.openwebrx-demodulator-button.disabled { + color: #B6B6B6; +} + .openwebrx-square-button img { height: 27px; diff --git a/htdocs/lib/Modes.js b/htdocs/lib/Modes.js index e7b2597..bf258d1 100644 --- a/htdocs/lib/Modes.js +++ b/htdocs/lib/Modes.js @@ -15,9 +15,8 @@ var Modes = { }, updateModePanel:function() { var available = this.modes.filter(function(m){ return m.isAvailable(); }); - var normalModes = available.filter(function(m){ return !m.digimode; }); - var digiModes = available.filter(function(m){ return m.digimode; }); - + var normalModes = available.filter(function(m){ return m.type === 'analog'; }); + var digiModes = available.filter(function(m){ return m.type === 'digimode'; }); var html = [] @@ -63,8 +62,14 @@ var Modes = { var Mode = function(json){ this.modulation = json.modulation; this.name = json.name; - this.digimode = json.digimode; + this.type = json.type; this.requirements = json.requirements; + if (json.bandpass) { + this.bandpass = json.bandpass; + } + if (this.type === 'digimode') { + this.underlying = json.underlying; + } }; Mode.prototype.isAvailable = function(){ diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index cd08808..ea1b281 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -374,40 +374,10 @@ function Demodulator_default_analog(offset_frequency, subtype) { }; } }; - //Subtypes only define some filter parameters and the mod string sent to server, - //so you may set these parameters in your custom child class. - //Why? As of demodulation is done on the server, difference is mainly on the server side. - this.server_mod = subtype; - if (subtype === "lsb") { - this.low_cut = -3000; - this.high_cut = -300; - this.server_mod = "ssb"; - } - else if (subtype === "usb") { - this.low_cut = 300; - this.high_cut = 3000; - this.server_mod = "ssb"; - } - else if (subtype === "cw") { - this.low_cut = 700; - this.high_cut = 900; - this.server_mod = "ssb"; - } - else if (subtype === "nfm") { - this.low_cut = -4000; - this.high_cut = 4000; - } - else if (subtype === "dmr" || subtype === "ysf") { - this.low_cut = -4000; - this.high_cut = 4000; - } - else if (subtype === "dstar" || subtype === "nxdn") { - this.low_cut = -3250; - this.high_cut = 3250; - } - else if (subtype === "am") { - this.low_cut = -4000; - this.high_cut = 4000; + var mode = Modes.findByModulation(subtype); + if (mode) { + this.low_cut = mode.bandpass.low_cut; + this.high_cut = mode.bandpass.high_cut; } this.wait_for_timer = false; @@ -433,7 +403,7 @@ function Demodulator_default_analog(offset_frequency, subtype) { "high_cut": this.high_cut, "offset_freq": this.offset_frequency }; - if (first_time) params.mod = this.server_mod; + if (first_time) params.mod = this.subtype; ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); mkenvelopes(get_visible_freq_range()); }; @@ -1174,6 +1144,7 @@ function on_ws_recv(evt) { break; case 'modes': Modes.setModes(json['value']); + demodulator_buttons_update(); break; default: console.warn('received message of unknown type: ' + json['type']); @@ -1947,29 +1918,24 @@ function initPanels() { } function demodulator_buttons_update() { - $(".openwebrx-demodulator-button").removeClass("highlighted"); + var $buttons = $(".openwebrx-demodulator-button"); + $buttons.removeClass("highlighted").removeClass('disabled'); if (!demodulators.length) return; + var mod = demodulators[0].subtype; + $("#openwebrx-button-" + mod).addClass("highlighted"); if (secondary_demod) { $("#openwebrx-button-dig").addClass("highlighted"); $('#openwebrx-secondary-demod-listbox').val(secondary_demod); - } else switch (demodulators[0].subtype) { - case "lsb": - case "usb": - case "cw": - if (demodulators[0].high_cut - demodulators[0].low_cut < 300) - $("#openwebrx-button-cw").addClass("highlighted"); - else { - if (demodulators[0].high_cut < 0) - $("#openwebrx-button-lsb").addClass("highlighted"); - else if (demodulators[0].low_cut > 0) - $("#openwebrx-button-usb").addClass("highlighted"); - else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted"); - } - break; - default: - var mod = demodulators[0].subtype; - $("#openwebrx-button-" + mod).addClass("highlighted"); - break; + var mode = Modes.findByModulation(secondary_demod); + if (mode) { + var active = mode.underlying.map(function(u){ return 'openwebrx-button-' + u; }); + console.info(active); + $buttons.filter(function(){ + console.info(this.id); + console.info(active.indexOf(this.id)); + return this.id !== "openwebrx-button-dig" && active.indexOf(this.id) < 0; + }).addClass('disabled'); + } } } @@ -2013,41 +1979,19 @@ function demodulator_digital_replace_last() { function demodulator_digital_replace(subtype) { if (secondary_demod === subtype) return; var mode = Modes.findByModulation(subtype); - if (mode && !mode.isAvailable()) { + if (!mode) { + return; + } + if (!mode.isAvailable()) { divlog('Digital mode "' + mode.name + '" not supported. Please check requirements', true); return; } - switch (subtype) { - case "bpsk31": - case "bpsk63": - case "rtty": - case "ft8": - case "jt65": - case "jt9": - case "ft4": - case "js8": - secondary_demod_start(subtype); - demodulator_analog_replace('usb', true); - break; - case "wspr": - secondary_demod_start(subtype); - demodulator_analog_replace('usb', true); - // WSPR only samples between 1400 and 1600 Hz - demodulators[0].low_cut = 1350; - demodulators[0].high_cut = 1650; - demodulators[0].set(); - break; - case "packet": - secondary_demod_start(subtype); - demodulator_analog_replace('nfm', true); - break; - case "pocsag": - secondary_demod_start(subtype); - demodulator_analog_replace('nfm', true); - demodulators[0].low_cut = -6000; - demodulators[0].high_cut = 6000; - demodulators[0].set(); - break; + secondary_demod_start(subtype); + demodulator_analog_replace(mode.underlying[0], true); + if (mode.bandpass) { + demodulators[0].low_cut = mode.bandpass.low_cut; + demodulators[0].high_cut = mode.bandpass.high_cut; + demodulators[0].set(); } demodulator_buttons_update(); $('#openwebrx-panel-digimodes').attr('data-mode', subtype); diff --git a/owrx/connection.py b/owrx/connection.py index 3d54ee8..8deff4f 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -11,7 +11,7 @@ from owrx.bookmarks import Bookmarks from owrx.map import Map from owrx.locator import Locator from owrx.property import PropertyStack -from owrx.modes import Modes +from owrx.modes import Modes, DigitalMode from multiprocessing import Queue from queue import Full from js8py import Js8Frame @@ -114,18 +114,18 @@ class OpenWebRxReceiverClient(Client): receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"]) self.write_receiver_details(receiver_info) - # TODO unsubscribe - receiver_details.wire(send_receiver_info) - send_receiver_info() - - self.__sendProfiles() - features = FeatureDetector().feature_availability() self.write_features(features) modes = Modes.getModes() self.write_modes(modes) + # TODO unsubscribe + receiver_details.wire(send_receiver_info) + send_receiver_info() + + self.__sendProfiles() + CpuUsageThread.getSharedInstance().add_client(self) def __sendProfiles(self): @@ -350,12 +350,23 @@ class OpenWebRxReceiverClient(Client): }}) def write_modes(self, modes): - self.send({"type": "modes", "value": [{ - "modulation": m.modulation, - "name": m.name, - "digimode": m.digimode, - "requirements": m.requirements - } for m in modes]}) + def to_json(m): + res = { + "modulation": m.modulation, + "name": m.name, + "type": "digimode" if isinstance(m, DigitalMode) else "analog", + "requirements": m.requirements + } + if m.bandpass is not None: + res["bandpass"] = { + "low_cut": m.bandpass.low_cut, + "high_cut": m.bandpass.high_cut + } + if isinstance(m, DigitalMode): + res["underlying"] = m.underlying + return res + + self.send({"type": "modes", "value": [to_json(m) for m in modes]}) class MapConnection(Client): diff --git a/owrx/modes.py b/owrx/modes.py index d2d890a..80fe94b 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -2,13 +2,19 @@ from owrx.feature import FeatureDetector from functools import reduce +class Bandpass(object): + def __init__(self, low_cut, high_cut): + self.low_cut = low_cut + self.high_cut = high_cut + + class Mode(object): - def __init__(self, modulation, name, requirements=None, service=False, digimode=False): + def __init__(self, modulation, name, bandpass: Bandpass = None, requirements=None, service=False): self.modulation = modulation self.name = name - self.digimode = digimode self.requirements = requirements if requirements is not None else [] self.service = service + self.bandpass = bandpass def is_available(self): fd = FeatureDetector() @@ -20,26 +26,37 @@ class Mode(object): return self.service +class AnalogMode(Mode): + pass + + +class DigitalMode(Mode): + def __init__(self, modulation, name, underlying, bandpass: Bandpass = None, requirements=None, service=False): + super().__init__(modulation, name, bandpass, requirements, service) + self.underlying = underlying + + class Modes(object): mappings = [ - Mode("nfm", "FM"), - Mode("am", "AM"), - Mode("lsb", "LSB"), - Mode("usb", "USB"), - Mode("cw", "CW"), - Mode("dmr", "DMR", requirements=["digital_voice_digiham"]), - Mode("dstar", "DStar", requirements=["digital_voice_dsd"]), - Mode("nxdn", "NXDN", requirements=["digital_voice_dsd"]), - Mode("ysf", "YSF", requirements=["digital_voice_digiham"]), - Mode("bpsk31", "BPSK31", digimode=True), - Mode("bpsk63", "BPSK63", digimode=True), - Mode("ft8", "FT8", requirements=["wsjt-x"], service=True, digimode=True), - Mode("ft4", "FT4", requirements=["wsjt-x"], service=True, digimode=True), - Mode("jt65", "JT65", requirements=["wsjt-x"], service=True, digimode=True), - Mode("jt9", "JT9", requirements=["wsjt-x"], service=True, digimode=True), - Mode("wspr", "WSPR", requirements=["wsjt-x"], service=True, digimode=True), - Mode("packet", "Packet", ["packet"], service=True, digimode=True), - Mode("js8", "JS8Call", requirements=["js8call"], service=True, digimode=True), + AnalogMode("nfm", "FM", bandpass=Bandpass(-4000, 4000)), + AnalogMode("am", "AM", bandpass=Bandpass(-4000, 4000)), + AnalogMode("lsb", "LSB", bandpass=Bandpass(-3000, -300)), + AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)), + AnalogMode("cw", "CW", bandpass=Bandpass(700, 900)), + AnalogMode("dmr", "DMR", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"]), + AnalogMode("dstar", "DStar", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"]), + AnalogMode("nxdn", "NXDN", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"]), + AnalogMode("ysf", "YSF", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"]), + DigitalMode("bpsk31", "BPSK31", underlying=["usb"]), + DigitalMode("bpsk63", "BPSK63", underlying=["usb"]), + DigitalMode("ft8", "FT8", underlying=["usb"], requirements=["wsjt-x"], service=True), + DigitalMode("ft4", "FT4", underlying=["usb"], requirements=["wsjt-x"], service=True), + DigitalMode("jt65", "JT65", underlying=["usb"], requirements=["wsjt-x"], service=True), + DigitalMode("jt9", "JT9", underlying=["usb"], requirements=["wsjt-x"], service=True), + DigitalMode("wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True), + DigitalMode("js8", "JS8Call", underlying=["usb"], requirements=["js8call"], service=True), + DigitalMode("packet", "Packet", underlying=["nfm", "usb", "lsb"], requirements=["packet"], service=True), + DigitalMode("pocsag", "Pocsag", underlying=["nfm"], bandpass=Bandpass(-6000, 6000), requirements=["pocsag"]) ] @staticmethod From 6a8168025dd7a531032bccdfaf350ec156bd2c6f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 26 Apr 2020 22:46:30 +0200 Subject: [PATCH 254/475] improve demodulator initialization --- htdocs/openwebrx.js | 16 ++++++++++++---- owrx/dsp.py | 19 +++++++++++++++++-- owrx/modes.py | 6 ++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index ea1b281..04598bb 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1048,7 +1048,7 @@ function on_ws_recv(evt) { updateSquelch(); waterfall_init(); - initialize_demodulator(initial_demodulator_params); + synchronize_demodulator_init({initialParams: initial_demodulator_params}); bookmarks.loadLocalBookmarks(); waterfall_clear(); @@ -1093,6 +1093,7 @@ function on_ws_recv(evt) { break; case "features": Modes.setFeatures(json['value']); + synchronize_demodulator_init({features: true}); break; case "metadata": update_metadata(json['value']); @@ -1144,6 +1145,7 @@ function on_ws_recv(evt) { break; case 'modes': Modes.setModes(json['value']); + synchronize_demodulator_init({modes: true}); demodulator_buttons_update(); break; default: @@ -1539,6 +1541,15 @@ function onAudioStart(success, apiType){ updateVolume(); } +var sync_params = {} + +function synchronize_demodulator_init(params) { + sync_params = $.extend(sync_params, params); + if (sync_params.initialParams && sync_params.modes && sync_params.features) { + initialize_demodulator(sync_params.initialParams); + } +} + function initialize_demodulator(initialParams) { mkscale(); var params = $.extend(initialParams || {}, validateHash()); @@ -1929,10 +1940,7 @@ function demodulator_buttons_update() { var mode = Modes.findByModulation(secondary_demod); if (mode) { var active = mode.underlying.map(function(u){ return 'openwebrx-button-' + u; }); - console.info(active); $buttons.filter(function(){ - console.info(this.id); - console.info(active.indexOf(this.id)); return this.id !== "openwebrx-button-dig" && active.indexOf(this.id) < 0; }).addClass('disabled'); } diff --git a/owrx/dsp.py b/owrx/dsp.py index 98d2b00..1f8850e 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -5,6 +5,7 @@ from owrx.aprs import AprsParser from owrx.pocsag import PocsagParser from owrx.source import SdrSource from owrx.property import PropertyStack, PropertyLayer +from owrx.modes import Modes from csdr import csdr import threading @@ -51,6 +52,8 @@ class DspManager(csdr.output): "digital_voice_unvoiced_quality", "temporary_directory", "center_freq", + "start_mod", + "start_freq", )) self.dsp = csdr.dsp(self) @@ -71,6 +74,20 @@ class DspManager(csdr.output): for parser in self.parsers.values(): parser.setDialFrequency(freq) + if "start_mod" in self.props: + self.dsp.set_demodulator(self.props["start_mod"]) + mode = Modes.findByModulation(self.props["start_mod"]) + + if mode and mode.bandpass: + self.dsp.set_bpf(mode.bandpass.low_cut, mode.bandpass.high_cut) + else: + self.dsp.set_bpf(-4000, 4000) + + if "start_freq" in self.props and "center_freq" in self.props: + self.dsp.set_offset_freq(self.props["start_freq"] - self.props["center_freq"]) + else: + self.dsp.set_offset_freq(0) + self.subscriptions = [ self.props.wireProperty("audio_compression", self.dsp.set_audio_compression), self.props.wireProperty("fft_compression", self.dsp.set_fft_compression), @@ -89,8 +106,6 @@ class DspManager(csdr.output): self.props.filter("center_freq", "offset_freq").wire(set_dial_freq), ] - self.dsp.set_offset_freq(0) - self.dsp.set_bpf(-4000, 4000) self.dsp.csdr_dynamic_bufsize = self.props["csdr_dynamic_bufsize"] self.dsp.csdr_print_bufsizes = self.props["csdr_print_bufsizes"] self.dsp.csdr_through = self.props["csdr_through"] diff --git a/owrx/modes.py b/owrx/modes.py index 80fe94b..7617b5f 100644 --- a/owrx/modes.py +++ b/owrx/modes.py @@ -70,3 +70,9 @@ class Modes(object): @staticmethod def getAvailableServices(): return [m for m in Modes.getAvailableModes() if m.is_service()] + + @staticmethod + def findByModulation(modulation): + modes = [m for m in Modes.getAvailableModes() if m.modulation == modulation] + if modes: + return modes[0] \ No newline at end of file From 25a7bbd86a96d75041ff3d32320ad14eac166e38 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 26 Apr 2020 23:14:34 +0200 Subject: [PATCH 255/475] reset secondary demodulator, too --- htdocs/openwebrx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 04598bb..24b5cc2 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1494,7 +1494,7 @@ function parseHash() { function validateHash() { var params = parseHash(); params = Object.keys(params).filter(function(key) { - if (key == 'freq' || key == 'mod') { + if (key == 'freq' || key == 'mod' || key == 'secondary_mod') { return params.freq && Math.abs(params.freq - center_freq) < bandwidth; } return true; From f1dc9af651ee376b9da75e1923783359f5fa7f34 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 27 Apr 2020 22:49:24 +0200 Subject: [PATCH 256/475] use synchronized setup; start dsp later --- htdocs/openwebrx.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 24b5cc2..15c7f24 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1445,10 +1445,6 @@ function on_ws_opened() { "type": "connectionproperties", "params": {"output_rate": audioEngine.getOutputRate()} })); - ws.send(JSON.stringify({ - "type": "dspcontrol", - "action": "start" - })); } var was_error = 0; @@ -1530,7 +1526,7 @@ function onAudioStart(success, apiType){ divlog('Web Audio API succesfully initialized, using ' + apiType + ' API, sample rate: ' + audioEngine.getSampleRate() + " Hz"); // canvas_container is set after waterfall_init() has been called. we cannot initialize before. - if (canvas_container) initialize_demodulator(); + if (canvas_container) synchronize_demodulator_init(); //hide log panel in a second (if user has not hidden it yet) window.setTimeout(function () { @@ -1544,7 +1540,7 @@ function onAudioStart(success, apiType){ var sync_params = {} function synchronize_demodulator_init(params) { - sync_params = $.extend(sync_params, params); + sync_params = $.extend(sync_params, params || {}); if (sync_params.initialParams && sync_params.modes && sync_params.features) { initialize_demodulator(sync_params.initialParams); } @@ -1561,6 +1557,10 @@ function initialize_demodulator(initialParams) { if (params.offset_frequency) { demodulators[0].set_offset_frequency(params.offset_frequency); } + ws.send(JSON.stringify({ + "type": "dspcontrol", + "action": "start" + })); } var reconnect_timeout = false; @@ -1804,7 +1804,7 @@ function openwebrx_init() { bookmarks = new BookmarkBar(); initSliders(); window.addEventListener('hashchange', function() { - initialize_demodulator(); + synchronize_demodulator_init(); }); } From 33762574c3b09552c0b457b63af34b1abdeb711b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 30 Apr 2020 22:07:19 +0200 Subject: [PATCH 257/475] improve demodulator initalization, part 2: refactor js classes --- htdocs/index.html | 1 + htdocs/lib/Demodulator.js | 217 +++++++++++++++++++++++++++++++++++++ htdocs/openwebrx.js | 220 +++----------------------------------- owrx/connection.py | 70 ++++++------ owrx/dsp.py | 5 + 5 files changed, 275 insertions(+), 238 deletions(-) create mode 100644 htdocs/lib/Demodulator.js diff --git a/htdocs/index.html b/htdocs/index.html index f6d08ac..98916dd 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -27,6 +27,7 @@ + diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js new file mode 100644 index 0000000..66a7196 --- /dev/null +++ b/htdocs/lib/Demodulator.js @@ -0,0 +1,217 @@ +function Filter() { + this.min_passband = 100; +} + +Filter.prototype.getLimits = function() { + var max_bw; + if (secondary_demod === 'pocsag') { + max_bw = 12500; + } else { + max_bw = (audioEngine.getOutputRate() / 2) - 1; + } + return { + high: max_bw, + low: -max_bw + }; +}; + +function Envelope(parent) { + this.parent = parent; + this.dragged_range = Demodulator.draggable_ranges.none; +} + +Envelope.prototype.draw = function(visible_range){ + this.visible_range = visible_range; + this.drag_ranges = demod_envelope_draw( + range, + center_freq + this.parent.offset_frequency + this.parent.low_cut, + center_freq + this.parent.offset_frequency + this.parent.high_cut, + this.color, center_freq + this.parent.offset_frequency + ); +}; + +Envelope.prototype.drag_start = function(x, key_modifiers){ + this.key_modifiers = key_modifiers; + this.dragged_range = demod_envelope_where_clicked(x, this.drag_ranges, key_modifiers); + this.drag_origin = { + x: x, + low_cut: this.parent.low_cut, + high_cut: this.parent.high_cut, + offset_frequency: this.parent.offset_frequency + }; + return this.dragged_range !== Demodulator.draggable_ranges.none; +}; + +Envelope.prototype.drag_move = function(x) { + var dr = Demodulator.draggable_ranges; + var new_value; + if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all + var freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x)); + + //dragging the line in the middle of the filter envelope while holding Shift does emulate + //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged + //Filter passband moves in the opposite direction than dragged, hence the minus below. + var minus = (this.dragged_range === dr.bfo) ? -1 : 1; + //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob + //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset + //frequency. + if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { + //we don't let low_cut go beyond its limits + if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.parent.filter.getLimits().low) return true; + //nor the filter passband be too small + if (this.parent.high_cut - new_value < this.parent.filter.min_passband) return true; + //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" + if (new_value >= this.parent.high_cut) return true; + this.parent.low_cut = new_value; + } + if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { + //we don't let high_cut go beyond its limits + if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.parent.filter.getLimits().high) return true; + //nor the filter passband be too small + if (new_value - this.parent.low_cut < this.parent.filter.min_passband) return true; + //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" + if (new_value <= this.parent.low_cut) return true; + this.parent.high_cut = new_value; + } + if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) { + //when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it) + new_value = this.drag_origin.offset_frequency + freq_change; + if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-) + this.parent.offset_frequency = new_value; + } + //now do the actual modifications: + mkenvelopes(this.visible_range); + this.parent.set(); + //will have to change this when changing to multi-demodulator mode: + tunedFrequencyDisplay.setFrequency(center_freq + this.parent.offset_frequency); + return true; +}; + +Envelope.prototype.drag_end = function(){ + demodulator_buttons_update(); + var to_return = this.dragged_range !== Demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset + this.dragged_range = Demodulator.draggable_ranges.none; + return to_return; +}; + + +//******* class Demodulator_default_analog ******* +// This can be used as a base for basic audio demodulators. +// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB + +function Demodulator(offset_frequency, modulation) { + this.offset_frequency = offset_frequency; + this.envelope = new Envelope(this); + this.color = Demodulator.get_next_color(); + this.modulation = modulation; + this.filter = new Filter(); + this.squelch_level = -150; + this.dmr_filter = 3; + this.state = {}; + var mode = Modes.findByModulation(modulation); + if (mode) { + this.low_cut = mode.bandpass.low_cut; + this.high_cut = mode.bandpass.high_cut; + } +} + +//ranges on filter envelope that can be dragged: +Demodulator.draggable_ranges = { + none: 0, + beginning: 1 /*from*/, + ending: 2 /*to*/, + anything_else: 3, + bfo: 4 /*line (while holding shift)*/, + pbs: 5 +}; //to which parameter these correspond in demod_envelope_draw() + +Demodulator.color_index = 0; +Demodulator.colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"]; + +Demodulator.get_next_color = function() { + if (this.color_index >= this.colors.length) this.color_index = 0; + return (this.colors[this.color_index++]); +} + + + +Demodulator.prototype.set_offset_frequency = function(to_what) { + if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return; + this.offset_frequency = Math.round(to_what); + this.set(); + mkenvelopes(get_visible_freq_range()); + tunedFrequencyDisplay.setFrequency(center_freq + to_what); + updateHash(); +}; + +Demodulator.prototype.get_offset_frequency = function() { + return this.offset_frequency; +}; + +Demodulator.prototype.get_modulation = function() { + return this.modulation; +}; + +Demodulator.prototype.start = function() { + this.set(); + ws.send(JSON.stringify({ + "type": "dspcontrol", + "action": "start" + })); +}; + +// TODO check if this is actually used +Demodulator.prototype.stop = function() { +}; + +Demodulator.prototype.send = function(params) { + ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); +} + +Demodulator.prototype.set = function () { //this function sends demodulator parameters to the server + var params = { + "low_cut": this.low_cut, + "high_cut": this.high_cut, + "offset_freq": this.offset_frequency, + "mod": this.modulation, + "dmr_filter": this.dmr_filter, + "squelch_level": this.squelch_level + }; + var to_send = {}; + for (var key in params) { + if (!(key in this.state) || params[key] !== this.state[key]) { + to_send[key] = params[key]; + } + } + if (Object.keys(to_send).length > 0) { + this.send(to_send); + for (var key in to_send) { + this.state[key] = to_send[key]; + } + } + mkenvelopes(get_visible_freq_range()); +}; + +Demodulator.prototype.setSquelch = function(squelch) { + this.squelch_level = squelch; + this.set(); +}; + +Demodulator.prototype.setDmrFilter = function(dmr_filter) { + this.dmr_filter = dmr_filter; + this.set(); +}; + +Demodulator.prototype.setBandpass = function(bandpass) { + this.bandpass = bandpass; + this.low_cut = bandpass.low_cut; + this.high_cut = bandpass.high_cut; + this.set(); +}; + +Demodulator.prototype.getBandpass = function() { + return { + low_cut: this.low_cut, + high_cut: this.high_cut + }; +}; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 15c7f24..1dcdd3f 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -142,7 +142,7 @@ function setSquelchToAuto() { function updateSquelch() { var sliderValue = parseInt($("#openwebrx-panel-squelch").val()); - ws.send(JSON.stringify({"type": "dspcontrol", "params": {"squelch_level": sliderValue}})); + if (demodulators[0]) demodulators[0].setSquelch(sliderValue); } var waterfall_min_level; @@ -238,14 +238,6 @@ function typeInAnimation(element, timeout, what, onFinish) { demodulators = []; -var demodulator_color_index = 0; -var demodulator_colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"]; - -function demodulators_get_next_color() { - if (demodulator_color_index >= demodulator_colors.length) demodulator_color_index = 0; - return (demodulator_colors[demodulator_color_index++]); -} - function demod_envelope_draw(range, from, to, color, line) { // ____ // Draws a standard filter envelope like this: _/ \_ // Parameters are given in offset frequency (Hz). @@ -331,166 +323,6 @@ function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) { // Check return dr.none; //User doesn't drag the envelope for this demodulator } -//******* class Demodulator ******* -// this can be used as a base class for ANY demodulator -Demodulator = function (offset_frequency) { - this.offset_frequency = offset_frequency; - this.envelope = {}; - this.color = demodulators_get_next_color(); - this.stop = function () { - }; -}; -//ranges on filter envelope that can be dragged: -Demodulator.draggable_ranges = { - none: 0, - beginning: 1 /*from*/, - ending: 2 /*to*/, - anything_else: 3, - bfo: 4 /*line (while holding shift)*/, - pbs: 5 -}; //to which parameter these correspond in demod_envelope_draw() - -//******* class Demodulator_default_analog ******* -// This can be used as a base for basic audio demodulators. -// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB - -demodulator_response_time = 50; - -function Demodulator_default_analog(offset_frequency, subtype) { - Demodulator.call(this, offset_frequency); - this.subtype = subtype; - this.filter = { - min_passband: 100, - getLimits: function() { - var max_bw; - if (secondary_demod === 'pocsag') { - max_bw = 12500; - } else { - max_bw = (audioEngine.getOutputRate() / 2) - 1; - } - return { - high: max_bw, - low: -max_bw - }; - } - }; - var mode = Modes.findByModulation(subtype); - if (mode) { - this.low_cut = mode.bandpass.low_cut; - this.high_cut = mode.bandpass.high_cut; - } - - this.wait_for_timer = false; - this.set_after = false; - this.set = function () { //set() is a wrapper to call doset(), but it ensures that doset won't execute more frequently than demodulator_response_time. - if (!this.wait_for_timer) { - this.doset(false); - this.set_after = false; - this.wait_for_timer = true; - var timeout_this = this; //http://stackoverflow.com/a/2130411 - window.setTimeout(function () { - timeout_this.wait_for_timer = false; - if (timeout_this.set_after) timeout_this.set(); - }, demodulator_response_time); - } else { - this.set_after = true; - } - }; - - this.doset = function (first_time) { //this function sends demodulator parameters to the server - var params = { - "low_cut": this.low_cut, - "high_cut": this.high_cut, - "offset_freq": this.offset_frequency - }; - if (first_time) params.mod = this.subtype; - ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); - mkenvelopes(get_visible_freq_range()); - }; - this.doset(true); //we set parameters on object creation - - //******* envelope object ******* - // for drawing the filter envelope above scale - this.envelope.parent = this; - - this.envelope.draw = function (visible_range) { - this.visible_range = visible_range; - this.drag_ranges = demod_envelope_draw(range, - center_freq + this.parent.offset_frequency + this.parent.low_cut, - center_freq + this.parent.offset_frequency + this.parent.high_cut, - this.color, center_freq + this.parent.offset_frequency); - }; - - this.envelope.dragged_range = Demodulator.draggable_ranges.none; - - // event handlers - this.envelope.drag_start = function (x, key_modifiers) { - this.key_modifiers = key_modifiers; - this.dragged_range = demod_envelope_where_clicked(x, this.drag_ranges, key_modifiers); - this.drag_origin = { - x: x, - low_cut: this.parent.low_cut, - high_cut: this.parent.high_cut, - offset_frequency: this.parent.offset_frequency - }; - return this.dragged_range !== Demodulator.draggable_ranges.none; - }; - - this.envelope.drag_move = function (x) { - var dr = Demodulator.draggable_ranges; - var new_value; - if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all - var freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x)); - - //dragging the line in the middle of the filter envelope while holding Shift does emulate - //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged - //Filter passband moves in the opposite direction than dragged, hence the minus below. - var minus = (this.dragged_range === dr.bfo) ? -1 : 1; - //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob - //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset - //frequency. - if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { - //we don't let low_cut go beyond its limits - if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.parent.filter.getLimits().low) return true; - //nor the filter passband be too small - if (this.parent.high_cut - new_value < this.parent.filter.min_passband) return true; - //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" - if (new_value >= this.parent.high_cut) return true; - this.parent.low_cut = new_value; - } - if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { - //we don't let high_cut go beyond its limits - if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.parent.filter.getLimits().high) return true; - //nor the filter passband be too small - if (new_value - this.parent.low_cut < this.parent.filter.min_passband) return true; - //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" - if (new_value <= this.parent.low_cut) return true; - this.parent.high_cut = new_value; - } - if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) { - //when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it) - new_value = this.drag_origin.offset_frequency + freq_change; - if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-) - this.parent.offset_frequency = new_value; - } - //now do the actual modifications: - mkenvelopes(this.visible_range); - this.parent.set(); - //will have to change this when changing to multi-demodulator mode: - tunedFrequencyDisplay.setFrequency(center_freq + this.parent.offset_frequency); - return true; - }; - - this.envelope.drag_end = function () { //in this demodulator we've already changed values in the drag_move() function so we shouldn't do too much here. - demodulator_buttons_update(); - var to_return = this.dragged_range !== Demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset - this.dragged_range = Demodulator.draggable_ranges.none; - return to_return; - }; - -} - -Demodulator_default_analog.prototype = new Demodulator(); function mkenvelopes(visible_range) //called from mkscale { @@ -498,7 +330,10 @@ function mkenvelopes(visible_range) //called from mkscale for (var i = 0; i < demodulators.length; i++) { demodulators[i].envelope.draw(visible_range); } - if (demodulators.length) secondary_demod_waterfall_set_zoom(demodulators[0].low_cut, demodulators[0].high_cut); + if (demodulators.length) { + var bandpass = demodulators[0].getBandpass() + secondary_demod_waterfall_set_zoom(bandpass.low_cut, bandpass.high_cut); + } } function demodulator_remove(which) { @@ -519,33 +354,22 @@ function demodulator_analog_replace(subtype, for_digital) { //this function shou secondary_demod_close_window(); secondary_demod_listbox_update(); } - if (!demodulators || !demodulators[0] || demodulators[0].subtype !== subtype) { + if (!demodulators || !demodulators[0] || demodulators[0].get_modulation() !== subtype) { last_analog_demodulator_subtype = subtype; var temp_offset = 0; if (demodulators.length) { - temp_offset = demodulators[0].offset_frequency; + temp_offset = demodulators[0].get_offset_frequency(); demodulator_remove(0); } - demodulator_add(new Demodulator_default_analog(temp_offset, subtype)); + var demod = new Demodulator(temp_offset, subtype); + demod.start(); + demodulator_add(demod); } demodulator_buttons_update(); update_digitalvoice_panels("openwebrx-panel-metadata-" + subtype); updateHash(); } -Demodulator.prototype.set_offset_frequency = function(to_what) { - if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return; - this.offset_frequency = Math.round(to_what); - this.set(); - mkenvelopes(get_visible_freq_range()); - tunedFrequencyDisplay.setFrequency(center_freq + to_what); - updateHash(); -} - -Demodulator.prototype.get_offset_frequency = function() { - return this.offset_frequency; -} - function waterfallWidth() { return $('body').width(); } @@ -988,7 +812,7 @@ function zoom_set(level) { level = parseInt(level); zoom_level = level; //zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+waterfallWidth()/2); //zoom to screen center instead of demod envelope - zoom_center_rel = demodulators[0].offset_frequency; + zoom_center_rel = demodulators[0].get_offset_frequency(); zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack resize_canvases(true); mkscale(); @@ -1468,12 +1292,6 @@ var mute = false; // Optimalise these if audio lags or is choppy: var audio_buffer_maximal_length_sec = 1; //actual number of samples are calculated from sample rate -function webrx_set_param(what, value) { - var params = {}; - params[what] = value; - ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); -} - function parseHash() { if (!window.location.hash) { return {}; @@ -1512,7 +1330,7 @@ function updateHash() { if (!demod) return; window.location.hash = $.map({ freq: demod.get_offset_frequency() + center_freq, - mod: demod.subtype, + mod: demod.get_modulation(), secondary_mod: secondary_demod }, function(value, key){ if (!value) return undefined; @@ -1543,6 +1361,7 @@ function synchronize_demodulator_init(params) { sync_params = $.extend(sync_params, params || {}); if (sync_params.initialParams && sync_params.modes && sync_params.features) { initialize_demodulator(sync_params.initialParams); + delete sync_params.initialParams; } } @@ -1557,10 +1376,7 @@ function initialize_demodulator(initialParams) { if (params.offset_frequency) { demodulators[0].set_offset_frequency(params.offset_frequency); } - ws.send(JSON.stringify({ - "type": "dspcontrol", - "action": "start" - })); + demodulators[0].start() } var reconnect_timeout = false; @@ -1836,7 +1652,7 @@ function update_dmr_timeslot_filtering() { }).toArray().reduce(function (acc, v) { return acc | v; }, 0); - webrx_set_param("dmr_filter", filter); + demodulators[0].setDmrFilter(filter); } function playButtonClick() { @@ -1932,7 +1748,7 @@ function demodulator_buttons_update() { var $buttons = $(".openwebrx-demodulator-button"); $buttons.removeClass("highlighted").removeClass('disabled'); if (!demodulators.length) return; - var mod = demodulators[0].subtype; + var mod = demodulators[0].get_modulation(); $("#openwebrx-button-" + mod).addClass("highlighted"); if (secondary_demod) { $("#openwebrx-button-dig").addClass("highlighted"); @@ -1997,9 +1813,7 @@ function demodulator_digital_replace(subtype) { secondary_demod_start(subtype); demodulator_analog_replace(mode.underlying[0], true); if (mode.bandpass) { - demodulators[0].low_cut = mode.bandpass.low_cut; - demodulators[0].high_cut = mode.bandpass.high_cut; - demodulators[0].set(); + demodulators[0].setBandpass(mode.bandpass); } demodulator_buttons_update(); $('#openwebrx-panel-digimodes').attr('data-mode', subtype); diff --git a/owrx/connection.py b/owrx/connection.py index 8deff4f..07c32ae 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -139,6 +139,7 @@ class OpenWebRxReceiverClient(Client): def handleTextMessage(self, conn, message): try: message = json.loads(message) + logger.debug(message) if "type" in message: if message["type"] == "dspcontrol": if "action" in message and message["action"] == "start": @@ -146,7 +147,7 @@ class OpenWebRxReceiverClient(Client): if "params" in message: params = message["params"] - self.setDspProperties(params) + self.getDsp().setProperties(params) elif message["type"] == "config": if "params" in message: @@ -163,7 +164,7 @@ class OpenWebRxReceiverClient(Client): if "params" in message: self.connectionProperties = message["params"] if self.dsp: - self.setDspProperties(self.connectionProperties) + self.getDsp().setProperties(self.connectionProperties) else: logger.warning("received message without type: {0}".format(message)) @@ -172,39 +173,30 @@ class OpenWebRxReceiverClient(Client): logger.warning("message is not json: {0}".format(message)) def setSdr(self, id=None): - while True: - next = None - if id is not None: - next = SdrService.getSource(id) - if next is None: - next = SdrService.getFirstSource() - if next is None: - # exit condition: no sdrs available - self.handleNoSdrsAvailable() - return + next = None + if id is not None: + next = SdrService.getSource(id) + if next is None: + next = SdrService.getFirstSource() + if next is None: + # exit condition: no sdrs available + self.handleNoSdrsAvailable() + return - # exit condition: no change - if next == self.sdr: - return + # exit condition: no change + if next == self.sdr: + return - self.stopDsp() + self.stopDsp() - if self.configSub is not None: - self.configSub.cancel() - self.configSub = None + if self.configSub is not None: + self.configSub.cancel() + self.configSub = None - self.sdr = next - - self.startDsp() - - # keep trying until we find a suitable SDR - if self.sdr.getState() == SdrSource.STATE_FAILED: - self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName())) - else: - break + self.sdr = next # send initial config - self.setDspProperties(self.connectionProperties) + self.getDsp().setProperties(self.connectionProperties) stack = PropertyStack() stack.addLayer(0, self.sdr.getProps()) @@ -236,9 +228,14 @@ class OpenWebRxReceiverClient(Client): self.write_sdr_error("No SDR Devices available") def startDsp(self): - if self.dsp is None and self.sdr is not None: - self.dsp = DspManager(self, self.sdr) - self.dsp.start() + while True: + logger.debug("starting dsp...") + self.getDsp().start() + if self.sdr.getState() == SdrSource.STATE_FAILED: + self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName())) + self.setSdr() + else: + break def close(self): self.stopDsp() @@ -250,6 +247,7 @@ class OpenWebRxReceiverClient(Client): super().close() def stopDsp(self): + logger.debug("stopDsp") if self.dsp is not None: self.dsp.stop() self.dsp = None @@ -270,9 +268,11 @@ class OpenWebRxReceiverClient(Client): for key, value in params.items(): protected[key] = value - def setDspProperties(self, params): - for key, value in params.items(): - self.dsp.setProperty(key, value) + def getDsp(self): + if self.dsp is None: + logger.debug("new DSP") + self.dsp = DspManager(self, self.sdr) + return self.dsp def write_spectrum_data(self, data): self.mp_send(bytes([0x01]) + data) diff --git a/owrx/dsp.py b/owrx/dsp.py index 1f8850e..979011e 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -135,6 +135,7 @@ class DspManager(csdr.output): super().__init__() def start(self): + logger.debug(self.sdrSource) if self.sdrSource.isAvailable(): self.dsp.start() @@ -160,6 +161,10 @@ class DspManager(csdr.output): sub.cancel() self.subscriptions = [] + def setProperties(self, props): + for k, v in props.items(): + self.setProperty(k, v) + def setProperty(self, prop, value): self.props[prop] = value From 1764abe65f509b0f1692a3238bd01a3e2a110475 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 30 Apr 2020 22:31:18 +0200 Subject: [PATCH 258/475] update secondary parameters --- htdocs/lib/Demodulator.js | 15 ++++++++++++++- htdocs/openwebrx.js | 13 ++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js index 66a7196..3938514 100644 --- a/htdocs/lib/Demodulator.js +++ b/htdocs/lib/Demodulator.js @@ -108,6 +108,7 @@ function Demodulator(offset_frequency, modulation) { this.squelch_level = -150; this.dmr_filter = 3; this.state = {}; + this.secondary_demod = false; var mode = Modes.findByModulation(modulation); if (mode) { this.low_cut = mode.bandpass.low_cut; @@ -175,7 +176,9 @@ Demodulator.prototype.set = function () { //this function sends demodulator par "offset_freq": this.offset_frequency, "mod": this.modulation, "dmr_filter": this.dmr_filter, - "squelch_level": this.squelch_level + "squelch_level": this.squelch_level, + "secondary_mod": this.secondary_demod, + "secondary_offset_freq": this.secondary_offset_freq }; var to_send = {}; for (var key in params) { @@ -215,3 +218,13 @@ Demodulator.prototype.getBandpass = function() { high_cut: this.high_cut }; }; + +Demodulator.prototype.set_secondary_demod = function(secondary_demod) { + this.secondary_demod = secondary_demod; + this.set(); +}; + +Demodulator.prototype.set_secondary_offset_freq = function(secondary_offset) { + this.secondary_offset_freq = secondary_offset; + this.set(); +}; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 1dcdd3f..2c8d13a 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1810,8 +1810,8 @@ function demodulator_digital_replace(subtype) { divlog('Digital mode "' + mode.name + '" not supported. Please check requirements', true); return; } - secondary_demod_start(subtype); demodulator_analog_replace(mode.underlying[0], true); + secondary_demod_start(subtype); if (mode.bandpass) { demodulators[0].setBandpass(mode.bandpass); } @@ -1879,12 +1879,14 @@ function secondary_demod_init() { function secondary_demod_start(subtype) { secondary_demod_canvases_initialized = false; - ws.send(JSON.stringify({"type": "dspcontrol", "params": {"secondary_mod": subtype}})); + demodulators[0].set_secondary_demod(subtype); secondary_demod = subtype; } function secondary_demod_stop() { - ws.send(JSON.stringify({"type": "dspcontrol", "params": {"secondary_mod": false}})); + if (demodulators[0]) { + demodulators[0].set_secondary_demod(false); + } secondary_demod = false; } @@ -1969,10 +1971,7 @@ function secondary_demod_update_channel_freq_from_event(evt) { if (!secondary_demod_waiting_for_set) { secondary_demod_waiting_for_set = true; window.setTimeout(function () { - ws.send(JSON.stringify({ - "type": "dspcontrol", - "params": {"secondary_offset_freq": Math.floor(secondary_demod_channel_freq)} - })); + demodulators[0].set_secondary_offset_freq(Math.floor(secondary_demod_channel_freq)); secondary_demod_waiting_for_set = false; }, 50 From 56f3f089a143db656f9a3ac527c3533667756d69 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 30 Apr 2020 22:54:44 +0200 Subject: [PATCH 259/475] fix debugging; synchronize startup --- owrx/connection.py | 3 --- owrx/dsp.py | 10 ++++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/owrx/connection.py b/owrx/connection.py index 07c32ae..bd020ed 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -229,7 +229,6 @@ class OpenWebRxReceiverClient(Client): def startDsp(self): while True: - logger.debug("starting dsp...") self.getDsp().start() if self.sdr.getState() == SdrSource.STATE_FAILED: self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName())) @@ -247,7 +246,6 @@ class OpenWebRxReceiverClient(Client): super().close() def stopDsp(self): - logger.debug("stopDsp") if self.dsp is not None: self.dsp.stop() self.dsp = None @@ -270,7 +268,6 @@ class OpenWebRxReceiverClient(Client): def getDsp(self): if self.dsp is None: - logger.debug("new DSP") self.dsp = DspManager(self, self.sdr) return self.dsp diff --git a/owrx/dsp.py b/owrx/dsp.py index 979011e..cc94097 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -130,14 +130,17 @@ class DspManager(csdr.output): self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq), ] + self.startOnAvailable = False + self.sdrSource.addClient(self) super().__init__() def start(self): - logger.debug(self.sdrSource) if self.sdrSource.isAvailable(): self.dsp.start() + else: + self.startOnAvailable = True def receive_output(self, t, read_fn): logger.debug("adding new output of type %s", t) @@ -156,6 +159,7 @@ class DspManager(csdr.output): def stop(self): self.dsp.stop() + self.startOnAvailable = False self.sdrSource.removeClient(self) for sub in self.subscriptions: sub.cancel() @@ -174,7 +178,9 @@ class DspManager(csdr.output): def onStateChange(self, state): if state == SdrSource.STATE_RUNNING: logger.debug("received STATE_RUNNING, attempting DspSource restart") - self.dsp.start() + if self.startOnAvailable: + self.dsp.start() + self.startOnAvailable = False elif state == SdrSource.STATE_STOPPING: logger.debug("received STATE_STOPPING, shutting down DspSource") self.dsp.stop() From 1441b9610ce9982d3eb149cd1501009393b8ae75 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 30 Apr 2020 23:16:49 +0200 Subject: [PATCH 260/475] refactor into the classes, too --- htdocs/lib/Demodulator.js | 91 ++++++++++++++++++++++++++++++++++++++- htdocs/openwebrx.js | 85 ------------------------------------ 2 files changed, 89 insertions(+), 87 deletions(-) diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js index 3938514..09bdd0a 100644 --- a/htdocs/lib/Demodulator.js +++ b/htdocs/lib/Demodulator.js @@ -22,7 +22,7 @@ function Envelope(parent) { Envelope.prototype.draw = function(visible_range){ this.visible_range = visible_range; - this.drag_ranges = demod_envelope_draw( + this.drag_ranges = this.envelope_draw( range, center_freq + this.parent.offset_frequency + this.parent.low_cut, center_freq + this.parent.offset_frequency + this.parent.high_cut, @@ -30,9 +30,73 @@ Envelope.prototype.draw = function(visible_range){ ); }; +Envelope.prototype.envelope_draw = function (range, from, to, color, line) { + // ____ + // Draws a standard filter envelope like this: _/ \_ + // Parameters are given in offset frequency (Hz). + // Envelope is drawn on the scale canvas. + // A "drag range" object is returned, containing information about the draggable areas of the envelope + // (beginning, ending and the line showing the offset frequency). + if (typeof color === "undefined") color = "#ffff00"; //yellow + var env_bounding_line_w = 5; // + var env_att_w = 5; // _______ ___env_h2 in px ___|_____ + var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_ + var env_h2 = 5; // |||env_att_line_w |_env_lineplus + var env_lineplus = 1; // ||env_bounding_line_w + var env_line_click_area = 6; + //range=get_visible_freq_range(); + var from_px = scale_px_from_freq(from, range); + var to_px = scale_px_from_freq(to, range); + if (to_px < from_px) /* swap'em */ { + var temp_px = to_px; + to_px = from_px; + from_px = temp_px; + } + + /*from_px-=env_bounding_line_w/2; + to_px+=env_bounding_line_w/2;*/ + from_px -= (env_att_w + env_bounding_line_w); + to_px += (env_att_w + env_bounding_line_w); + // do drawing: + scale_ctx.lineWidth = 3; + scale_ctx.strokeStyle = color; + scale_ctx.fillStyle = color; + var drag_ranges = {envelope_on_screen: false, line_on_screen: false}; + if (!(to_px < 0 || from_px > window.innerWidth)) // out of screen? + { + drag_ranges.beginning = {x1: from_px, x2: from_px + env_bounding_line_w + env_att_w}; + drag_ranges.ending = {x1: to_px - env_bounding_line_w - env_att_w, x2: to_px}; + drag_ranges.whole_envelope = {x1: from_px, x2: to_px}; + drag_ranges.envelope_on_screen = true; + scale_ctx.beginPath(); + scale_ctx.moveTo(from_px, env_h1); + scale_ctx.lineTo(from_px + env_bounding_line_w, env_h1); + scale_ctx.lineTo(from_px + env_bounding_line_w + env_att_w, env_h2); + scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2); + scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1); + scale_ctx.lineTo(to_px, env_h1); + scale_ctx.globalAlpha = 0.3; + scale_ctx.fill(); + scale_ctx.globalAlpha = 1; + scale_ctx.stroke(); + } + if (typeof line !== "undefined") // out of screen? + { + var line_px = scale_px_from_freq(line, range); + if (!(line_px < 0 || line_px > window.innerWidth)) { + drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2}; + drag_ranges.line_on_screen = true; + scale_ctx.moveTo(line_px, env_h1 + env_lineplus); + scale_ctx.lineTo(line_px, env_h2 - env_lineplus); + scale_ctx.stroke(); + } + } + return drag_ranges; +}; + Envelope.prototype.drag_start = function(x, key_modifiers){ this.key_modifiers = key_modifiers; - this.dragged_range = demod_envelope_where_clicked(x, this.drag_ranges, key_modifiers); + this.dragged_range = this.where_clicked(x, this.drag_ranges, key_modifiers); this.drag_origin = { x: x, low_cut: this.parent.low_cut, @@ -42,6 +106,29 @@ Envelope.prototype.drag_start = function(x, key_modifiers){ return this.dragged_range !== Demodulator.draggable_ranges.none; }; +Envelope.prototype.where_clicked = function(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by demod_envelope_draw(). + var in_range = function (x, range) { + return range.x1 <= x && range.x2 >= x; + }; + var dr = Demodulator.draggable_ranges; + + if (key_modifiers.shiftKey) { + //Check first: shift + center drag emulates BFO knob + if (drag_ranges.line_on_screen && in_range(x, drag_ranges.line)) return dr.bfo; + //Check second: shift + envelope drag emulates PBF knob + if (drag_ranges.envelope_on_screen && in_range(x, drag_ranges.whole_envelope)) return dr.pbs; + } + if (drag_ranges.envelope_on_screen) { + // For low and high cut: + if (in_range(x, drag_ranges.beginning)) return dr.beginning; + if (in_range(x, drag_ranges.ending)) return dr.ending; + // Last priority: having clicked anything else on the envelope, without holding the shift key + if (in_range(x, drag_ranges.whole_envelope)) return dr.anything_else; + } + return dr.none; //User doesn't drag the envelope for this demodulator +}; + + Envelope.prototype.drag_move = function(x) { var dr = Demodulator.draggable_ranges; var new_value; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 2c8d13a..0f7537f 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -238,91 +238,6 @@ function typeInAnimation(element, timeout, what, onFinish) { demodulators = []; -function demod_envelope_draw(range, from, to, color, line) { // ____ - // Draws a standard filter envelope like this: _/ \_ - // Parameters are given in offset frequency (Hz). - // Envelope is drawn on the scale canvas. - // A "drag range" object is returned, containing information about the draggable areas of the envelope - // (beginning, ending and the line showing the offset frequency). - if (typeof color === "undefined") color = "#ffff00"; //yellow - var env_bounding_line_w = 5; // - var env_att_w = 5; // _______ ___env_h2 in px ___|_____ - var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_ - var env_h2 = 5; // |||env_att_line_w |_env_lineplus - var env_lineplus = 1; // ||env_bounding_line_w - var env_line_click_area = 6; - //range=get_visible_freq_range(); - var from_px = scale_px_from_freq(from, range); - var to_px = scale_px_from_freq(to, range); - if (to_px < from_px) /* swap'em */ { - var temp_px = to_px; - to_px = from_px; - from_px = temp_px; - } - - /*from_px-=env_bounding_line_w/2; - to_px+=env_bounding_line_w/2;*/ - from_px -= (env_att_w + env_bounding_line_w); - to_px += (env_att_w + env_bounding_line_w); - // do drawing: - scale_ctx.lineWidth = 3; - scale_ctx.strokeStyle = color; - scale_ctx.fillStyle = color; - var drag_ranges = {envelope_on_screen: false, line_on_screen: false}; - if (!(to_px < 0 || from_px > window.innerWidth)) // out of screen? - { - drag_ranges.beginning = {x1: from_px, x2: from_px + env_bounding_line_w + env_att_w}; - drag_ranges.ending = {x1: to_px - env_bounding_line_w - env_att_w, x2: to_px}; - drag_ranges.whole_envelope = {x1: from_px, x2: to_px}; - drag_ranges.envelope_on_screen = true; - scale_ctx.beginPath(); - scale_ctx.moveTo(from_px, env_h1); - scale_ctx.lineTo(from_px + env_bounding_line_w, env_h1); - scale_ctx.lineTo(from_px + env_bounding_line_w + env_att_w, env_h2); - scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2); - scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1); - scale_ctx.lineTo(to_px, env_h1); - scale_ctx.globalAlpha = 0.3; - scale_ctx.fill(); - scale_ctx.globalAlpha = 1; - scale_ctx.stroke(); - } - if (typeof line !== "undefined") // out of screen? - { - var line_px = scale_px_from_freq(line, range); - if (!(line_px < 0 || line_px > window.innerWidth)) { - drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2}; - drag_ranges.line_on_screen = true; - scale_ctx.moveTo(line_px, env_h1 + env_lineplus); - scale_ctx.lineTo(line_px, env_h2 - env_lineplus); - scale_ctx.stroke(); - } - } - return drag_ranges; -} - -function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by demod_envelope_draw(). - var in_range = function (x, range) { - return range.x1 <= x && range.x2 >= x; - }; - var dr = Demodulator.draggable_ranges; - - if (key_modifiers.shiftKey) { - //Check first: shift + center drag emulates BFO knob - if (drag_ranges.line_on_screen && in_range(x, drag_ranges.line)) return dr.bfo; - //Check second: shift + envelope drag emulates PBF knob - if (drag_ranges.envelope_on_screen && in_range(x, drag_ranges.whole_envelope)) return dr.pbs; - } - if (drag_ranges.envelope_on_screen) { - // For low and high cut: - if (in_range(x, drag_ranges.beginning)) return dr.beginning; - if (in_range(x, drag_ranges.ending)) return dr.ending; - // Last priority: having clicked anything else on the envelope, without holding the shift key - if (in_range(x, drag_ranges.whole_envelope)) return dr.anything_else; - } - return dr.none; //User doesn't drag the envelope for this demodulator -} - function mkenvelopes(visible_range) //called from mkscale { From 02a632660558a2e9b7273e059ba94d515bbe12e5 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 30 Apr 2020 23:20:56 +0200 Subject: [PATCH 261/475] fix method names in comments --- htdocs/lib/Demodulator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js index 09bdd0a..24448de 100644 --- a/htdocs/lib/Demodulator.js +++ b/htdocs/lib/Demodulator.js @@ -106,7 +106,7 @@ Envelope.prototype.drag_start = function(x, key_modifiers){ return this.dragged_range !== Demodulator.draggable_ranges.none; }; -Envelope.prototype.where_clicked = function(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by demod_envelope_draw(). +Envelope.prototype.where_clicked = function(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by envelope_draw(). var in_range = function (x, range) { return range.x1 <= x && range.x2 >= x; }; @@ -211,7 +211,7 @@ Demodulator.draggable_ranges = { anything_else: 3, bfo: 4 /*line (while holding shift)*/, pbs: 5 -}; //to which parameter these correspond in demod_envelope_draw() +}; //to which parameter these correspond in envelope_draw() Demodulator.color_index = 0; Demodulator.colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"]; From 5013af2117ad2d5e576441099cf15f8106fc896a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 30 Apr 2020 23:31:52 +0200 Subject: [PATCH 262/475] combine methods --- htdocs/lib/Demodulator.js | 49 +++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js index 24448de..e742028 100644 --- a/htdocs/lib/Demodulator.js +++ b/htdocs/lib/Demodulator.js @@ -15,29 +15,21 @@ Filter.prototype.getLimits = function() { }; }; -function Envelope(parent) { - this.parent = parent; +function Envelope(demodulator) { + this.demodulator = demodulator; this.dragged_range = Demodulator.draggable_ranges.none; } Envelope.prototype.draw = function(visible_range){ this.visible_range = visible_range; - this.drag_ranges = this.envelope_draw( - range, - center_freq + this.parent.offset_frequency + this.parent.low_cut, - center_freq + this.parent.offset_frequency + this.parent.high_cut, - this.color, center_freq + this.parent.offset_frequency - ); -}; + var line = center_freq + this.demodulator.offset_frequency; -Envelope.prototype.envelope_draw = function (range, from, to, color, line) { // ____ // Draws a standard filter envelope like this: _/ \_ // Parameters are given in offset frequency (Hz). // Envelope is drawn on the scale canvas. // A "drag range" object is returned, containing information about the draggable areas of the envelope // (beginning, ending and the line showing the offset frequency). - if (typeof color === "undefined") color = "#ffff00"; //yellow var env_bounding_line_w = 5; // var env_att_w = 5; // _______ ___env_h2 in px ___|_____ var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_ @@ -45,7 +37,9 @@ Envelope.prototype.envelope_draw = function (range, from, to, color, line) { var env_lineplus = 1; // ||env_bounding_line_w var env_line_click_area = 6; //range=get_visible_freq_range(); + var from = center_freq + this.demodulator.offset_frequency + this.demodulator.low_cut; var from_px = scale_px_from_freq(from, range); + var to = center_freq + this.demodulator.offset_frequency + this.demodulator.high_cut; var to_px = scale_px_from_freq(to, range); if (to_px < from_px) /* swap'em */ { var temp_px = to_px; @@ -53,12 +47,11 @@ Envelope.prototype.envelope_draw = function (range, from, to, color, line) { from_px = temp_px; } - /*from_px-=env_bounding_line_w/2; - to_px+=env_bounding_line_w/2;*/ from_px -= (env_att_w + env_bounding_line_w); to_px += (env_att_w + env_bounding_line_w); // do drawing: scale_ctx.lineWidth = 3; + var color = this.color || '#ffff00'; // yellow scale_ctx.strokeStyle = color; scale_ctx.fillStyle = color; var drag_ranges = {envelope_on_screen: false, line_on_screen: false}; @@ -91,7 +84,7 @@ Envelope.prototype.envelope_draw = function (range, from, to, color, line) { scale_ctx.stroke(); } } - return drag_ranges; + this.drag_ranges = drag_ranges; }; Envelope.prototype.drag_start = function(x, key_modifiers){ @@ -99,9 +92,9 @@ Envelope.prototype.drag_start = function(x, key_modifiers){ this.dragged_range = this.where_clicked(x, this.drag_ranges, key_modifiers); this.drag_origin = { x: x, - low_cut: this.parent.low_cut, - high_cut: this.parent.high_cut, - offset_frequency: this.parent.offset_frequency + low_cut: this.demodulator.low_cut, + high_cut: this.demodulator.high_cut, + offset_frequency: this.demodulator.offset_frequency }; return this.dragged_range !== Demodulator.draggable_ranges.none; }; @@ -144,33 +137,33 @@ Envelope.prototype.drag_move = function(x) { //frequency. if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { //we don't let low_cut go beyond its limits - if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.parent.filter.getLimits().low) return true; + if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.demodulator.filter.getLimits().low) return true; //nor the filter passband be too small - if (this.parent.high_cut - new_value < this.parent.filter.min_passband) return true; + if (this.demodulator.high_cut - new_value < this.demodulator.filter.min_passband) return true; //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" - if (new_value >= this.parent.high_cut) return true; - this.parent.low_cut = new_value; + if (new_value >= this.demodulator.high_cut) return true; + this.demodulator.low_cut = new_value; } if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { //we don't let high_cut go beyond its limits - if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.parent.filter.getLimits().high) return true; + if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.demodulator.filter.getLimits().high) return true; //nor the filter passband be too small - if (new_value - this.parent.low_cut < this.parent.filter.min_passband) return true; + if (new_value - this.demodulator.low_cut < this.demodulator.filter.min_passband) return true; //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" - if (new_value <= this.parent.low_cut) return true; - this.parent.high_cut = new_value; + if (new_value <= this.demodulator.low_cut) return true; + this.demodulator.high_cut = new_value; } if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) { //when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it) new_value = this.drag_origin.offset_frequency + freq_change; if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-) - this.parent.offset_frequency = new_value; + this.demodulator.offset_frequency = new_value; } //now do the actual modifications: mkenvelopes(this.visible_range); - this.parent.set(); + this.demodulator.set(); //will have to change this when changing to multi-demodulator mode: - tunedFrequencyDisplay.setFrequency(center_freq + this.parent.offset_frequency); + tunedFrequencyDisplay.setFrequency(center_freq + this.demodulator.offset_frequency); return true; }; From b8f7686a6dcb6da2011348ea2daf670fd8763b5d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 2 May 2020 00:05:20 +0200 Subject: [PATCH 263/475] refactor demodulator classes, part 1 --- htdocs/css/openwebrx.css | 14 ++- htdocs/index.html | 5 +- htdocs/lib/BookmarkBar.js | 22 ++-- htdocs/lib/Demodulator.js | 41 ++++++-- htdocs/lib/DemodulatorPanel.js | 158 ++++++++++++++++++++++++++++ htdocs/lib/FrequencyDisplay.js | 25 +++-- htdocs/lib/Modes.js | 62 +++-------- htdocs/openwebrx.js | 185 +++++---------------------------- 8 files changed, 273 insertions(+), 239 deletions(-) create mode 100644 htdocs/lib/DemodulatorPanel.js diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 731bac1..981bf9c 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -311,7 +311,7 @@ input[type=range]:focus::-ms-fill-upper font-style: normal; } -#webrx-actual-freq { +.webrx-actual-freq { width: 100%; text-align: left; padding: 0; @@ -320,11 +320,11 @@ input[type=range]:focus::-ms-fill-upper flex-direction: row; } -#webrx-actual-freq > * { +.webrx-actual-freq > * { flex: 1; } -#webrx-actual-freq input { +.webrx-actual-freq input { font-family: 'roboto-mono'; width: 0; box-sizing: border-box; @@ -334,14 +334,13 @@ input[type=range]:focus::-ms-fill-upper color: inherit; } -#webrx-actual-freq, #webrx-actual-freq input { +.webrx-actual-freq, .webrx-actual-freq input { font-size: 16pt; font-family: 'roboto-mono'; line-height: 22px; } -#webrx-mouse-freq -{ +.webrx-mouse-freq { width: 100%; text-align: left; font-size: 10pt; @@ -728,8 +727,7 @@ img.openwebrx-mirror-img color: White; } -#openwebrx-secondary-demod-listbox -{ +.openwebrx-secondary-demod-listbox { width: 173px; height: 27px; padding-left:3px; diff --git a/htdocs/index.html b/htdocs/index.html index 98916dd..3c706f3 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -28,6 +28,7 @@ + @@ -156,8 +157,8 @@
-
-
+
+
- +
Cancel
diff --git a/htdocs/lib/BookmarkDialog.js b/htdocs/lib/BookmarkDialog.js new file mode 100644 index 0000000..09bfa1b --- /dev/null +++ b/htdocs/lib/BookmarkDialog.js @@ -0,0 +1,10 @@ +$.fn.bookmarkDialog = function() { + var $el = this; + return { + setModes: function(modes) { + $el.find('#modulation').html(modes.map(function(m) { + return ''; + }).join('')); + } + } +} \ No newline at end of file diff --git a/htdocs/lib/Modes.js b/htdocs/lib/Modes.js index a33cf4f..c68466a 100644 --- a/htdocs/lib/Modes.js +++ b/htdocs/lib/Modes.js @@ -5,6 +5,7 @@ var Modes = { setModes:function(json){ this.modes = json.map(function(m){ return new Mode(m); }); this.updatePanels(); + $('#openwebrx-dialog-bookmark').bookmarkDialog().setModes(this.modes); }, getModes:function(){ return this.modes; diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py index 88563e5..b7b272a 100644 --- a/owrx/controllers/assets.py +++ b/owrx/controllers/assets.py @@ -72,6 +72,7 @@ class CompiledAssetsController(Controller): "lib/Demodulator.js", "lib/DemodulatorPanel.js", "lib/BookmarkBar.js", + "lib/BookmarkDialog.js", "lib/AudioEngine.js", "lib/ProgressBar.js", "lib/Measurement.js", From e2cacc1fa0c9666d0d22af7b05f1f1ef8ca9b530 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 3 May 2020 23:58:12 +0200 Subject: [PATCH 293/475] only available ones --- htdocs/lib/BookmarkDialog.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/htdocs/lib/BookmarkDialog.js b/htdocs/lib/BookmarkDialog.js index 09bfa1b..6d2f113 100644 --- a/htdocs/lib/BookmarkDialog.js +++ b/htdocs/lib/BookmarkDialog.js @@ -2,7 +2,9 @@ $.fn.bookmarkDialog = function() { var $el = this; return { setModes: function(modes) { - $el.find('#modulation').html(modes.map(function(m) { + $el.find('#modulation').html(modes.filter(function(m){ + return m.isAvailable(); + }).map(function(m) { return ''; }).join('')); } From efa9771ad741d28fd6eb66bf3ad72392b37b2886 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 4 May 2020 00:20:01 +0200 Subject: [PATCH 294/475] let's move some logic to the dialog --- htdocs/lib/BookmarkBar.js | 22 ++++------------------ htdocs/lib/BookmarkDialog.js | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/htdocs/lib/BookmarkBar.js b/htdocs/lib/BookmarkBar.js index 653eb02..ecae43b 100644 --- a/htdocs/lib/BookmarkBar.js +++ b/htdocs/lib/BookmarkBar.js @@ -102,40 +102,26 @@ BookmarkBar.prototype.render = function(){ }; BookmarkBar.prototype.showEditDialog = function(bookmark) { - var $form = this.$dialog.find("form"); if (!bookmark) { bookmark = { name: "", frequency: center_freq + this.getDemodulator().get_offset_frequency(), - modulation: this.getDemodulator().get_modulation() + modulation: this.getDemodulator().get_secondary_demod() || this.getDemodulator().get_modulation() } } - ['name', 'frequency', 'modulation'].forEach(function(key){ - $form.find('#' + key).val(bookmark[key]); - }); - this.$dialog.data('id', bookmark.id); + this.$dialog.bookmarkDialog().setValues(bookmark); this.$dialog.show(); this.$dialog.find('#name').focus(); }; BookmarkBar.prototype.storeBookmark = function() { var me = this; - var bookmark = {}; - var valid = true; - ['name', 'frequency', 'modulation'].forEach(function(key){ - var $input = me.$dialog.find('#' + key); - valid = valid && $input[0].checkValidity(); - bookmark[key] = $input.val(); - }); - if (!valid) { - me.$dialog.find("form :submit").click(); - return; - } + var bookmark = this.$dialog.bookmarkDialog().getValues(); + if (!bookmark) return; bookmark.frequency = Number(bookmark.frequency); var bookmarks = me.localBookmarks.getBookmarks(); - bookmark.id = me.$dialog.data('id'); if (!bookmark.id) { if (bookmarks.length) { bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; })); diff --git a/htdocs/lib/BookmarkDialog.js b/htdocs/lib/BookmarkDialog.js index 6d2f113..4a0a184 100644 --- a/htdocs/lib/BookmarkDialog.js +++ b/htdocs/lib/BookmarkDialog.js @@ -7,6 +7,30 @@ $.fn.bookmarkDialog = function() { }).map(function(m) { return ''; }).join('')); + return this; + }, + setValues: function(bookmark) { + var $form = $el.find('form'); + ['name', 'frequency', 'modulation'].forEach(function(key){ + $form.find('#' + key).val(bookmark[key]); + }); + $el.data('id', bookmark.id || false); + return this; + }, + getValues: function() { + var bookmark = {}; + var valid = true; + ['name', 'frequency', 'modulation'].forEach(function(key){ + var $input = $el.find('#' + key); + valid = valid && $input[0].checkValidity(); + bookmark[key] = $input.val(); + }); + if (!valid) { + $el.find("form :submit").click(); + return; + } + bookmark.id = $el.data('id'); + return bookmark; } } } \ No newline at end of file From eab3bf780e04c388c8340d4e03cc17c7e4d250bd Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 4 May 2020 20:36:17 +0200 Subject: [PATCH 295/475] fix problems with sdr device failover detection --- owrx/connection.py | 67 +++++++++++++++++++++++++++------------------- owrx/websocket.py | 10 +++++-- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/owrx/connection.py b/owrx/connection.py index 4ac9386..a1c7c82 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -15,6 +15,7 @@ from owrx.modes import Modes, DigitalMode from multiprocessing import Queue from queue import Full from js8py import Js8Frame +from abc import ABC, abstractmethod import json import threading @@ -23,7 +24,7 @@ import logging logger = logging.getLogger(__name__) -class Client(object): +class Client(ABC): def __init__(self, conn): self.conn = conn self.multiprocessingPipe = Queue(100) @@ -52,6 +53,7 @@ class Client(object): except Full: self.close() + @abstractmethod def handleTextMessage(self, conn, message): pass @@ -145,8 +147,12 @@ class OpenWebRxReceiverClient(Client): self.startDsp() if "params" in message: - params = message["params"] - self.getDsp().setProperties(params) + dsp = self.getDsp() + if dsp is None: + logger.warning("DSP not available; discarding client data") + else: + params = message["params"] + dsp.setProperties(params) elif message["type"] == "config": if "params" in message: @@ -172,27 +178,38 @@ class OpenWebRxReceiverClient(Client): logger.warning("message is not json: {0}".format(message)) def setSdr(self, id=None): - next = None - if id is not None: - next = SdrService.getSource(id) - if next is None: - next = SdrService.getFirstSource() - if next is None: - # exit condition: no sdrs available - self.handleNoSdrsAvailable() - return + while True: + next = None + if id is not None: + next = SdrService.getSource(id) + if next is None: + next = SdrService.getFirstSource() + if next is None: + # exit condition: no sdrs available + logger.warning("no more SDR devices available") + self.handleNoSdrsAvailable() + return - # exit condition: no change - if next == self.sdr: - return + # exit condition: no change + if next == self.sdr: + return - self.stopDsp() + self.stopDsp() - if self.configSub is not None: - self.configSub.cancel() - self.configSub = None + if self.configSub is not None: + self.configSub.cancel() + self.configSub = None - self.sdr = next + self.sdr = next + + self.getDsp() + + # found a working sdr, exit the loop + if self.sdr.getState() != SdrSource.STATE_FAILED: + break + + logger.warning('SDR device "%s" has failed, selecing new device', self.sdr.getName()) + self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName())) # send initial config self.getDsp().setProperties(self.connectionProperties) @@ -227,13 +244,7 @@ class OpenWebRxReceiverClient(Client): self.write_sdr_error("No SDR Devices available") def startDsp(self): - while True: - self.getDsp().start() - if self.sdr.getState() == SdrSource.STATE_FAILED: - self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName())) - self.setSdr() - else: - break + self.getDsp().start() def close(self): self.stopDsp() @@ -271,7 +282,7 @@ class OpenWebRxReceiverClient(Client): pass def getDsp(self): - if self.dsp is None: + if self.dsp is None and self.sdr is not None: self.dsp = DspManager(self, self.sdr) return self.dsp diff --git a/owrx/websocket.py b/owrx/websocket.py index a5fb188..fc6462d 100644 --- a/owrx/websocket.py +++ b/owrx/websocket.py @@ -199,9 +199,15 @@ class WebSocketConnection(object): data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) if opcode == OPCODE_TEXT_MESSAGE: message = data.decode("utf-8") - self.messageHandler.handleTextMessage(self, message) + try: + self.messageHandler.handleTextMessage(self, message) + except Exception: + logger.exception("Exception in websocket handler handleTextMessage()") elif opcode == OPCODE_BINARY_MESSAGE: - self.messageHandler.handleBinaryMessage(self, data) + try: + self.messageHandler.handleBinaryMessage(self, data) + except Exception: + logger.exception("Exception in websocket handler handleBinaryMessage()") elif opcode == OPCODE_PING: self.sendPong() elif opcode == OPCODE_PONG: From 66a4f299116e6e23344e87477319ba73f2ecd0b6 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 6 May 2020 19:54:55 +0200 Subject: [PATCH 296/475] let's try pre-loading the pipes to improve dsp initialization --- csdr/csdr.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/csdr/csdr.py b/csdr/csdr.py index 57cd5cb..9d23978 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -639,8 +639,7 @@ class dsp(object): def set_offset_freq(self, offset_freq): self.offset_freq = offset_freq if self.running: - with self.modification_lock: - self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate)) + self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate)) def set_center_freq(self, center_freq): # dsp only needs to know this to be able to pass it to decoders in the form of get_operating_freq() @@ -653,10 +652,9 @@ class dsp(object): self.low_cut = low_cut self.high_cut = high_cut if self.running: - with self.modification_lock: - self.pipes["bpf_pipe"].write( - "%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate()) - ) + self.pipes["bpf_pipe"].write( + "%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate()) + ) def get_bpf(self): return [self.low_cut, self.high_cut] @@ -669,8 +667,7 @@ class dsp(object): # no squelch required on digital voice modes actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() else self.squelch_level if self.running: - with self.modification_lock: - self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch))) + self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch))) def set_unvoiced_quality(self, q): self.unvoiced_quality = q @@ -743,6 +740,16 @@ class dsp(object): # create control pipes for csdr self.try_create_pipes(self.pipe_names, command_base) + # send initial config through the pipes + if self.has_pipe("bpf_pipe"): + self.set_bpf(self.low_cut, self.high_cut) + if self.has_pipe("shift_pipe"): + self.set_offset_freq(self.offset_freq) + if self.has_pipe("squelch_pipe"): + self.set_squelch_level(self.squelch_level) + if self.has_pipe("dmr_control_pipe"): + self.set_dmr_filter(3) + # run the command command = command_base.format( bpf_pipe=self.pipes["bpf_pipe"], @@ -799,16 +806,6 @@ class dsp(object): self.start_secondary_demodulator() - # send initial config through the pipes - if self.has_pipe("bpf_pipe"): - self.set_bpf(self.low_cut, self.high_cut) - if self.has_pipe("shift_pipe"): - self.set_offset_freq(self.offset_freq) - if self.has_pipe("squelch_pipe"): - self.set_squelch_level(self.squelch_level) - if self.has_pipe("dmr_control_pipe"): - self.set_dmr_filter(3) - if self.has_pipe("smeter_pipe"): def read_smeter(): raw = self.pipes["smeter_pipe"].readline() From 7fbd024ed5ba5eec3d7f2c1ae1ea62ee5cb652e7 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 6 May 2020 22:52:48 +0200 Subject: [PATCH 297/475] fix sql=0 parameter --- htdocs/lib/DemodulatorPanel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js index bab7c02..a0506a1 100644 --- a/htdocs/lib/DemodulatorPanel.js +++ b/htdocs/lib/DemodulatorPanel.js @@ -212,7 +212,7 @@ DemodulatorPanel.prototype.transformHashParams = function(params) { mod: params.secondary_mod || params.mod }; if (params.offset_frequency) ret.offset_frequency = params.offset_frequency; - if (params.sql) ret.squelch_level = parseInt(params.sql); + if (typeof(params.sql) !== 'undefined') ret.squelch_level = parseInt(params.sql); return ret; }; @@ -299,7 +299,7 @@ DemodulatorPanel.prototype.updateHash = function() { secondary_mod: demod.get_secondary_demod(), sql: demod.getSquelch(), }, function(value, key){ - if (!value) return undefined; + if (typeof(value) === 'undefined') return undefined; return key + '=' + value; }).filter(function(v) { return !!v; From ceafcbf8506d336cedfa3f3e48f814eca0928523 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Wed, 6 May 2020 23:00:57 +0200 Subject: [PATCH 298/475] fix secondary demod being false --- htdocs/lib/DemodulatorPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js index a0506a1..40894e2 100644 --- a/htdocs/lib/DemodulatorPanel.js +++ b/htdocs/lib/DemodulatorPanel.js @@ -299,7 +299,7 @@ DemodulatorPanel.prototype.updateHash = function() { secondary_mod: demod.get_secondary_demod(), sql: demod.getSquelch(), }, function(value, key){ - if (typeof(value) === 'undefined') return undefined; + if (typeof(value) === 'undefined' || value === false) return undefined; return key + '=' + value; }).filter(function(v) { return !!v; From fc7188145b1a828c22e9e315480cc99efa5e0f9d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 8 May 2020 21:18:03 +0200 Subject: [PATCH 299/475] use jquery to store progressbar objects --- htdocs/index.html | 12 ++++++------ htdocs/lib/ProgressBar.js | 31 +++++++++++++++++++++++++++---- htdocs/openwebrx.js | 30 ++++++++++-------------------- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 6c1def2..9f899ae 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -134,12 +134,12 @@
-
Audio buffer [0 ms]
-
Audio output [0 sps]
-
Audio stream [0 kbps]
-
Network usage [0 kbps]
-
Server CPU [0%]
-
Clients [1]
+
Audio buffer [0 ms]
+
Audio output [0 sps]
+
Audio stream [0 kbps]
+
Network usage [0 kbps]
+
Server CPU [0%]
+
Clients [1]
diff --git a/htdocs/lib/ProgressBar.js b/htdocs/lib/ProgressBar.js index 9d0736d..0019dc6 100644 --- a/htdocs/lib/ProgressBar.js +++ b/htdocs/lib/ProgressBar.js @@ -25,13 +25,16 @@ ProgressBar.prototype.setOver = function(over) { this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6"); }; -AudioBufferProgressBar = function(el, sampleRate) { +AudioBufferProgressBar = function(el) { ProgressBar.call(this, el); - this.sampleRate = sampleRate; }; AudioBufferProgressBar.prototype = new ProgressBar(); +AudioBufferProgressBar.prototype.setSampleRate = function(sampleRate) { + this.sampleRate = sampleRate; +}; + AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) { var audio_buffer_value = buffersize / this.sampleRate; var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; @@ -70,12 +73,15 @@ AudioSpeedProgressBar.prototype.setSpeed = function(speed) { AudioOutputProgressBar = function(el, sampleRate) { ProgressBar.call(this, el); - this.maxRate = sampleRate * 1.25; - this.minRate = sampleRate * .25; }; AudioOutputProgressBar.prototype = new ProgressBar(); +AudioOutputProgressBar.prototype.setSampleRate = function(sampleRate) { + this.maxRate = sampleRate * 1.25; + this.minRate = sampleRate * .25; +}; + AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) { this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate); }; @@ -111,3 +117,20 @@ CpuProgressBar.prototype = new ProgressBar(); CpuProgressBar.prototype.setUsage = function(usage) { this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85); }; + +ProgressBar.types = { + cpu: CpuProgressBar, + audiobuffer: AudioBufferProgressBar, + audiospeed: AudioSpeedProgressBar, + audiooutput: AudioOutputProgressBar, + clients: ClientsProgressBar, + networkspeed: NetworkSpeedProgressBar +} + +$.fn.progressbar = function() { + if (!this.data('progressbar')) { + var constructor = ProgressBar.types[this.data('type')] || ProgressBar; + this.data('progressbar', new constructor(this)); + } + return this.data('progressbar'); +}; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 7b32731..4238ad6 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -739,7 +739,7 @@ function on_ws_recv(evt) { divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + "."); fft_compression = config['fft_compression']; divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + "."); - clientProgressBar.setMaxClients(config['max_clients']); + $('#openwebrx-bar-clients').progressbar().setMaxClients(config['max_clients']); waterfall_init(); var demodulatorPanel = $('#openwebrx-panel-receiver').demodulatorPanel(); @@ -773,10 +773,10 @@ function on_ws_recv(evt) { setSmeterAbsoluteValue(smeter_level); break; case "cpuusage": - cpuProgressBar.setUsage(json['value']); + $('#openwebrx-bar-server-cpu').progressbar().setUsage(json['value']); break; case "clients": - clientProgressBar.setClients(json['value']); + $('#openwebrx-bar-clients').progressbar().setClients(json['value']); break; case "profiles": var listbox = e("openwebrx-sdr-profiles-listbox"); @@ -1121,7 +1121,7 @@ function on_ws_opened() { if (!networkSpeedMeasurement) { networkSpeedMeasurement = new Measurement(); networkSpeedMeasurement.report(60000, 1000, function(rate){ - networkSpeedProgressBar.setSpeed(rate); + $('#openwebrx-bar-network-speed').progressbar().setSpeed(rate); }); } else { networkSpeedMeasurement.reset(); @@ -1352,33 +1352,23 @@ function init_header() { }); } -var audioBufferProgressBar; -var networkSpeedProgressBar; -var audioSpeedProgressBar; -var audioOutputProgressBar; -var clientProgressBar; -var cpuProgressBar; - function initProgressBars() { - audioBufferProgressBar = new AudioBufferProgressBar($('#openwebrx-bar-audio-buffer'), audioEngine.getSampleRate()); - networkSpeedProgressBar = new NetworkSpeedProgressBar($('#openwebrx-bar-network-speed')); - audioSpeedProgressBar = new AudioSpeedProgressBar($('#openwebrx-bar-audio-speed')); - audioOutputProgressBar = new AudioOutputProgressBar($('#openwebrx-bar-audio-output'), audioEngine.getSampleRate()); - clientProgressBar = new ClientsProgressBar($('#openwebrx-bar-clients')); - cpuProgressBar = new CpuProgressBar($('#openwebrx-bar-server-cpu')); + $('#openwebrx-bar-audio-buffer, #openwebrx-bar-audio-output').each(function() { + $(this).progressbar().setSampleRate(audioEngine.getSampleRate()); + }); } function audioReporter(stats) { if (typeof(stats.buffersize) !== 'undefined') { - audioBufferProgressBar.setBuffersize(stats.buffersize); + $('#openwebrx-bar-audio-buffer').progressbar().setBuffersize(stats.buffersize); } if (typeof(stats.audioByteRate) !== 'undefined') { - audioSpeedProgressBar.setSpeed(stats.audioByteRate * 8); + $('#openwebrx-bar-audio-speed').progressbar().setSpeed(stats.audioByteRate * 8); } if (typeof(stats.audioRate) !== 'undefined') { - audioOutputProgressBar.setAudioRate(stats.audioRate); + $('#openwebrx-bar-audio-output').progressbar().setAudioRate(stats.audioRate); } } From 9563adacf72c02c011b13e0d64828a7663722cca Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 8 May 2020 21:35:45 +0200 Subject: [PATCH 300/475] more jquery magic for progressbars --- htdocs/index.html | 12 ++++++------ htdocs/lib/ProgressBar.js | 33 +++++++++++++++++++++++++++++++-- htdocs/openwebrx.js | 9 ++++++--- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 9f899ae..1290402 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -134,12 +134,12 @@
-
Audio buffer [0 ms]
-
Audio output [0 sps]
-
Audio stream [0 kbps]
-
Network usage [0 kbps]
-
Server CPU [0%]
-
Clients [1]
+
+
+
+
+
+
diff --git a/htdocs/lib/ProgressBar.js b/htdocs/lib/ProgressBar.js index 0019dc6..59d3a91 100644 --- a/htdocs/lib/ProgressBar.js +++ b/htdocs/lib/ProgressBar.js @@ -1,10 +1,15 @@ ProgressBar = function(el) { this.$el = $(el); - this.$innerText = this.$el.find('.openwebrx-progressbar-text'); - this.$innerBar = this.$el.find('.openwebrx-progressbar-bar'); + this.$innerText = $('' + this.getDefaultText() + ''); + this.$innerBar = $('
'); + this.$el.empty().append(this.$innerText, this.$innerBar); this.$innerBar.css('width', '0%'); }; +ProgressBar.prototype.getDefaultText = function() { + return ''; +} + ProgressBar.prototype.set = function(val, text, over) { this.setValue(val); this.setText(text); @@ -31,6 +36,10 @@ AudioBufferProgressBar = function(el) { AudioBufferProgressBar.prototype = new ProgressBar(); +AudioBufferProgressBar.prototype.getDefaultText = function() { + return 'Audio buffer [0 ms]'; +}; + AudioBufferProgressBar.prototype.setSampleRate = function(sampleRate) { this.sampleRate = sampleRate; }; @@ -56,6 +65,10 @@ NetworkSpeedProgressBar = function(el) { NetworkSpeedProgressBar.prototype = new ProgressBar(); +NetworkSpeedProgressBar.prototype.getDefaultText = function() { + return 'Network usage [0 kbps]'; +}; + NetworkSpeedProgressBar.prototype.setSpeed = function(speed) { var speedInKilobits = speed * 8 / 1000; this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false); @@ -67,6 +80,10 @@ AudioSpeedProgressBar = function(el) { AudioSpeedProgressBar.prototype = new ProgressBar(); +AudioSpeedProgressBar.prototype.getDefaultText = function() { + return 'Audio stream [0 kbps]'; +}; + AudioSpeedProgressBar.prototype.setSpeed = function(speed) { this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false); }; @@ -77,6 +94,10 @@ AudioOutputProgressBar = function(el, sampleRate) { AudioOutputProgressBar.prototype = new ProgressBar(); +AudioOutputProgressBar.prototype.getDefaultText = function() { + return 'Audio output [0 sps]'; +}; + AudioOutputProgressBar.prototype.setSampleRate = function(sampleRate) { this.maxRate = sampleRate * 1.25; this.minRate = sampleRate * .25; @@ -94,6 +115,10 @@ ClientsProgressBar = function(el) { ClientsProgressBar.prototype = new ProgressBar(); +ClientsProgressBar.prototype.getDefaultText = function() { + return 'Clients [1]'; +}; + ClientsProgressBar.prototype.setClients = function(clients) { this.clients = clients; this.render(); @@ -114,6 +139,10 @@ CpuProgressBar = function(el) { CpuProgressBar.prototype = new ProgressBar(); +CpuProgressBar.prototype.getDefaultText = function() { + return 'Server CPU [0%]'; +}; + CpuProgressBar.prototype.setUsage = function(usage) { this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85); }; diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 4238ad6..8f2ca79 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1353,9 +1353,12 @@ function init_header() { } function initProgressBars() { - $('#openwebrx-bar-audio-buffer, #openwebrx-bar-audio-output').each(function() { - $(this).progressbar().setSampleRate(audioEngine.getSampleRate()); - }); + $(".openwebrx-progressbar").each(function(){ + var bar = $(this).progressbar(); + if ('setSampleRate' in bar) { + bar.setSampleRate(audioEngine.getSampleRate()); + } + }) } function audioReporter(stats) { From af1cfee754dfaa6fffbdf176d03147b8a532cf12 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 8 May 2020 22:56:02 +0200 Subject: [PATCH 301/475] allow switching underlying modulation (if available) refs #95 --- htdocs/lib/DemodulatorPanel.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js index 40894e2..6670c73 100644 --- a/htdocs/lib/DemodulatorPanel.js +++ b/htdocs/lib/DemodulatorPanel.js @@ -86,8 +86,8 @@ DemodulatorPanel.prototype.render = function() { this.el.find(".openwebrx-modes").html(html); }; -DemodulatorPanel.prototype.setMode = function(modulation) { - var mode = Modes.findByModulation(modulation); +DemodulatorPanel.prototype.setMode = function(requestedModulation) { + var mode = Modes.findByModulation(requestedModulation); if (!mode) { return; } @@ -101,6 +101,14 @@ DemodulatorPanel.prototype.setMode = function(modulation) { if (mode.type === 'digimode') { modulation = mode.underlying[0]; + } else { + if (this.mode && this.mode.type === 'digimode' && this.mode.underlying.indexOf(requestedModulation) >= 0) { + // keep the mode, just switch underlying modulation + mode = this.mode; + modulation = requestedModulation; + } else { + modulation = mode.modulation; + } } var current = this.collectParams(); @@ -112,6 +120,7 @@ DemodulatorPanel.prototype.setMode = function(modulation) { this.stopDemodulator(); this.demodulator = new Demodulator(current.offset_frequency, modulation); this.demodulator.setSquelch(current.squelch_level); + var self = this; var updateFrequency = function(freq) { self.tuneableFrequencyDisplay.setFrequency(self.center_freq + freq); @@ -144,6 +153,8 @@ DemodulatorPanel.prototype.setMode = function(modulation) { }; DemodulatorPanel.prototype.disableDigiMode = function() { + // just a little trick to get out of the digimode + delete this.mode; this.setMode(this.getDemodulator().get_modulation()); }; From 1f565355eca3463d9840c42fc5e7e2d6b2de10a7 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 8 May 2020 23:34:34 +0200 Subject: [PATCH 302/475] change available mode highlighting, refs #95 --- htdocs/css/openwebrx.css | 4 ++-- htdocs/lib/DemodulatorPanel.js | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index dbc0f14..fd81097 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -453,8 +453,8 @@ input[type=range]:disabled { margin-right: 5px; } -.openwebrx-demodulator-button.disabled { - color: #B6B6B6; +.openwebrx-demodulator-button.same-mod { + color: #FFC; } .openwebrx-square-button img diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js index 6670c73..bfd3619 100644 --- a/htdocs/lib/DemodulatorPanel.js +++ b/htdocs/lib/DemodulatorPanel.js @@ -233,7 +233,7 @@ DemodulatorPanel.prototype.squelchAvailable = function () { DemodulatorPanel.prototype.updateButtons = function() { var $buttons = this.el.find(".openwebrx-demodulator-button"); - $buttons.removeClass("highlighted").removeClass('disabled'); + $buttons.removeClass("highlighted").removeClass('same-mod'); var demod = this.getDemodulator() if (!demod) return; this.el.find('[data-modulation=' + demod.get_modulation() + ']').addClass("highlighted"); @@ -243,10 +243,12 @@ DemodulatorPanel.prototype.updateButtons = function() { this.el.find('.openwebrx-secondary-demod-listbox').val(secondary_demod); var mode = Modes.findByModulation(secondary_demod); if (mode) { - $buttons.filter(function(){ - var mod = $(this).data('modulation'); - return mod && mode.underlying.indexOf(mod) < 0; - }).addClass('disabled'); + var self = this; + mode.underlying.filter(function(m) { + return m !== demod.get_modulation(); + }).forEach(function(m) { + self.el.find('[data-modulation=' + m + ']').addClass('same-mod') + }); } } else { this.el.find('.openwebrx-secondary-demod-listbox').val('none'); From dba4f91c7712cf5b89cf0a3d2b83f39c9352d05d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 8 May 2020 23:49:02 +0200 Subject: [PATCH 303/475] include homepage --- htdocs/index.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/htdocs/index.html b/htdocs/index.html index 1290402..22f33bb 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -127,7 +127,10 @@
OpenWebRX client log
-
Author contact: Jakob Ketterl, DD5JFK
+
+ Author contact: Jakob Ketterl, DD5JFK | + OpenWebRX homepage +
Support and information: Groups.io Mailinglist
From f2288ceb4938c5638abcdc3cbdfb014b349496da Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 8 May 2020 23:53:50 +0200 Subject: [PATCH 304/475] let's work with frame targets --- htdocs/include/header.include.html | 2 +- htdocs/openwebrx.js | 6 +++--- owrx/controllers/template.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html index c86977c..aa2275a 100644 --- a/htdocs/include/header.include.html +++ b/htdocs/include/header.include.html @@ -16,7 +16,7 @@

Status

Log

Receiver
-
Map
+
Map
${settingslink}
diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 8f2ca79..0fa62cd 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -975,14 +975,14 @@ function update_wsjt_panel(msg) { if (['FT8', 'JT65', 'JT9', 'FT4'].indexOf(msg['mode']) >= 0) { matches = linkedmsg.match(/(.*\s[A-Z0-9]+\s)([A-R]{2}[0-9]{2})$/); if (matches && matches[2] !== 'RR73') { - linkedmsg = html_escape(matches[1]) + '' + matches[2] + ''; + linkedmsg = html_escape(matches[1]) + '' + matches[2] + ''; } else { linkedmsg = html_escape(linkedmsg); } } else if (msg['mode'] === 'WSPR') { matches = linkedmsg.match(/([A-Z0-9]*\s)([A-R]{2}[0-9]{2})(\s[0-9]+)/); if (matches) { - linkedmsg = html_escape(matches[1]) + '' + matches[2] + '' + html_escape(matches[3]); + linkedmsg = html_escape(matches[1]) + '' + matches[2] + '' + html_escape(matches[3]); } else { linkedmsg = html_escape(linkedmsg); } @@ -1068,7 +1068,7 @@ function update_packet_panel(msg) { 'style="' + stylesToString(styles) + '"' ].join(' '); if (msg.lat && msg.lon) { - link = '' + overlay + ''; + link = '' + overlay + ''; } else { link = '
' + overlay + '
' } diff --git a/owrx/controllers/template.py b/owrx/controllers/template.py index 6030fd1..23c1d17 100644 --- a/owrx/controllers/template.py +++ b/owrx/controllers/template.py @@ -23,7 +23,7 @@ class WebpageController(TemplateController): settingslink = "" pm = Config.get() if "webadmin_enabled" in pm and pm["webadmin_enabled"]: - settingslink = """
Settings
""" + settingslink = """
Settings
""" header = self.render_template("include/header.include.html", settingslink=settingslink) return {"header": header} From 3a455a0452afe6ee1e806023aeb9f16e1a97f416 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 9 May 2020 00:11:20 +0200 Subject: [PATCH 305/475] start collecting header routines --- htdocs/lib/Header.js | 22 ++++++++++++++++++++++ htdocs/openwebrx.js | 15 ++------------- owrx/controllers/assets.py | 1 + 3 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 htdocs/lib/Header.js diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js new file mode 100644 index 0000000..aec4ab7 --- /dev/null +++ b/htdocs/lib/Header.js @@ -0,0 +1,22 @@ +function Header(el) { + this.el = el; + + this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () { + toggle_panel($(this).data('toggle-panel')); + }); +}; + +Header.prototype.setDetails = function(details) { + this.el.find('#webrx-rx-title').html(details['receiver_name']); + var query = encodeURIComponent(details['receiver_gps']['lat'] + ',' + details['receiver_gps']['lon']); + this.el.find('#webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m, [maps]'); + this.el.find('#webrx-rx-photo-title').html(details['photo_title']); + this.el.find('#webrx-rx-photo-desc').html(details['photo_desc']); +}; + +$.fn.header = function() { + if (!this.data('header')) { + this.data('header', new Header(this)); + } + return this.data('header'); +}; \ No newline at end of file diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 0fa62cd..fb5586d 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -761,12 +761,7 @@ function on_ws_recv(evt) { secondary_demod_init_canvases(); break; case "receiver_details": - var r = json['value']; - e('webrx-rx-title').innerHTML = r['receiver_name']; - var query = encodeURIComponent(r['receiver_gps']['lat'] + ',' + r['receiver_gps']['lon']); - e('webrx-rx-desc').innerHTML = r['receiver_location'] + ' | Loc: ' + r['locator'] + ', ASL: ' + r['receiver_asl'] + ' m, [maps]'; - e('webrx-rx-photo-title').innerHTML = r['photo_title']; - e('webrx-rx-photo-desc').innerHTML = r['photo_desc']; + $('#webrx-top-container').header().setDetails(json['value']); break; case "smeter": smeter_level = json['value']; @@ -1346,12 +1341,6 @@ function openwebrx_resize() { resize_scale(); } -function init_header() { - $('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () { - toggle_panel($(this).data('toggle-panel')); - }); -} - function initProgressBars() { $(".openwebrx-progressbar").each(function(){ var bar = $(this).progressbar(); @@ -1397,7 +1386,7 @@ function openwebrx_init() { $('.webrx-mouse-freq').frequencyDisplay(); $('#openwebrx-panel-receiver').demodulatorPanel(); window.addEventListener("resize", openwebrx_resize); - init_header(); + $('#webrx-top-container').header(); bookmarks = new BookmarkBar(); initSliders(); } diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py index b7b272a..f0bf6ac 100644 --- a/owrx/controllers/assets.py +++ b/owrx/controllers/assets.py @@ -69,6 +69,7 @@ class CompiledAssetsController(Controller): "openwebrx.js", "lib/jquery-3.2.1.min.js", "lib/jquery.nanoscroller.js", + "lib/Header.js", "lib/Demodulator.js", "lib/DemodulatorPanel.js", "lib/BookmarkBar.js", From 3202f48f8e081090651fade75b9316b33c9e25df Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 9 May 2020 00:20:38 +0200 Subject: [PATCH 306/475] header details on map, too --- htdocs/map.html | 1 + htdocs/map.js | 5 +++++ owrx/connection.py | 55 ++++++++++++++++++++++++---------------------- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/htdocs/map.html b/htdocs/map.html index 23aabeb..dbfd13d 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -7,6 +7,7 @@ + diff --git a/htdocs/map.js b/htdocs/map.js index 948e83b..339d1d5 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -250,6 +250,11 @@ case "update": processUpdates(json.value); break; + case 'receiver_details': + $('#webrx-top-container').header().setDetails(json['value']); + break; + default: + console.warn('received message of unknown type: ' + json['type']); } } catch (e) { // don't lose exception diff --git a/owrx/connection.py b/owrx/connection.py index a1c7c82..14620fe 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -15,7 +15,7 @@ from owrx.modes import Modes, DigitalMode from multiprocessing import Queue from queue import Full from js8py import Js8Frame -from abc import ABC, abstractmethod +from abc import ABC, ABCMeta, abstractmethod import json import threading @@ -64,7 +64,33 @@ class Client(ABC): self.close() -class OpenWebRxReceiverClient(Client): +class OpenWebRxClient(Client, metaclass=ABCMeta): + def __init__(self, conn): + super().__init__(conn) + + receiver_details = Config.get().filter( + "receiver_name", + "receiver_location", + "receiver_asl", + "receiver_gps", + "photo_title", + "photo_desc", + ) + + def send_receiver_info(*args): + receiver_info = receiver_details.__dict__() + receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"]) + self.write_receiver_details(receiver_info) + + # TODO unsubscribe + receiver_details.wire(send_receiver_info) + send_receiver_info() + + def write_receiver_details(self, details): + self.send({"type": "receiver_details", "value": details}) + + +class OpenWebRxReceiverClient(OpenWebRxClient): config_keys = [ "waterfall_colors", "waterfall_min_level", @@ -97,34 +123,14 @@ class OpenWebRxReceiverClient(Client): self.close() raise - pm = Config.get() - self.setSdr() - receiver_details = pm.filter( - "receiver_name", - "receiver_location", - "receiver_asl", - "receiver_gps", - "photo_title", - "photo_desc", - ) - - def send_receiver_info(*args): - receiver_info = receiver_details.__dict__() - receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"]) - self.write_receiver_details(receiver_info) - features = FeatureDetector().feature_availability() self.write_features(features) modes = Modes.getModes() self.write_modes(modes) - # TODO unsubscribe - receiver_details.wire(send_receiver_info) - send_receiver_info() - self.__sendProfiles() CpuUsageThread.getSharedInstance().add_client(self) @@ -314,9 +320,6 @@ class OpenWebRxReceiverClient(Client): def write_config(self, cfg): self.send({"type": "config", "value": cfg}) - def write_receiver_details(self, details): - self.send({"type": "receiver_details", "value": details}) - def write_profiles(self, profiles): self.send({"type": "profiles", "value": profiles}) @@ -382,7 +385,7 @@ class OpenWebRxReceiverClient(Client): self.send({"type": "modes", "value": [to_json(m) for m in modes]}) -class MapConnection(Client): +class MapConnection(OpenWebRxClient): def __init__(self, conn): super().__init__(conn) From 3a5e227ab51d3afc94ef29debf39647b1b70920c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 9 May 2020 00:27:42 +0200 Subject: [PATCH 307/475] integrate feature report --- htdocs/css/features.css | 4 ++++ htdocs/features.html | 1 + htdocs/settings.html | 3 +++ 3 files changed, 8 insertions(+) diff --git a/htdocs/css/features.css b/htdocs/css/features.css index 7b0b008..8131da6 100644 --- a/htdocs/css/features.css +++ b/htdocs/css/features.css @@ -9,4 +9,8 @@ h1 { text-align: center; margin: 50px 0; +} + +.table { + color: inherit; } \ No newline at end of file diff --git a/htdocs/features.html b/htdocs/features.html index 8e0eb61..8ddc941 100644 --- a/htdocs/features.html +++ b/htdocs/features.html @@ -2,6 +2,7 @@ OpenWebRX Feature report + diff --git a/htdocs/settings.html b/htdocs/settings.html index 659b660..00fbe9e 100644 --- a/htdocs/settings.html +++ b/htdocs/settings.html @@ -22,5 +22,8 @@ ${header} +
\ No newline at end of file From 149ad8dcc6fbcfc5e511d1e870260556b17088a6 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 9 May 2020 01:03:43 +0200 Subject: [PATCH 308/475] move rx_photo code to header --- htdocs/css/map.css | 5 ---- htdocs/lib/Header.js | 55 ++++++++++++++++++++++++++++++++++++++++- htdocs/openwebrx.js | 58 -------------------------------------------- 3 files changed, 54 insertions(+), 64 deletions(-) diff --git a/htdocs/css/map.css b/htdocs/css/map.css index 5d478cd..de6ef3e 100644 --- a/htdocs/css/map.css +++ b/htdocs/css/map.css @@ -1,11 +1,6 @@ @import url("openwebrx-header.css"); @import url("openwebrx-globals.css"); -/* expandable photo not implemented on map page */ -#webrx-top-photo-clip { - max-height: 67px; -} - body { display: flex; flex-direction: column; diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js index aec4ab7..ebdc5b1 100644 --- a/htdocs/lib/Header.js +++ b/htdocs/lib/Header.js @@ -4,16 +4,69 @@ function Header(el) { this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () { toggle_panel($(this).data('toggle-panel')); }); + + this.init_rx_photo(); }; Header.prototype.setDetails = function(details) { this.el.find('#webrx-rx-title').html(details['receiver_name']); var query = encodeURIComponent(details['receiver_gps']['lat'] + ',' + details['receiver_gps']['lon']); - this.el.find('#webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m, [maps]'); + this.el.find('#webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m, [maps]'); this.el.find('#webrx-rx-photo-title').html(details['photo_title']); this.el.find('#webrx-rx-photo-desc').html(details['photo_desc']); }; +Header.prototype.init_rx_photo = function() { + var clip = this.el.find("#webrx-top-photo-clip")[0]; + this.rx_photo_height = clip.clientHeight; + clip.style.maxHeight = this.rx_photo_height + "px"; + this.rx_photo_state = 1; + + $.extend($.easing, { + easeOutCubic:function(x) { + return 1 - Math.pow( 1 - x, 3 ); + } + }); + + window.setTimeout(function () { + $('#webrx-rx-photo-title').animate({opacity: 0}, 500); + }, 1000); + window.setTimeout(function () { + $('#webrx-rx-photo-desc').animate({opacity: 0}, 500); + }, 1500); + window.setTimeout(this.close_rx_photo.bind(this), 2500); + $('#webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this)); +}; + +Header.prototype.close_rx_photo = function() { + this.rx_photo_state = 0; + this.el.find("#webrx-rx-photo-desc").animate({opacity: 0}); + this.el.find("#webrx-rx-photo-title").animate({opacity: 0}); + this.el.find('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'}); + this.el.find("#openwebrx-rx-details-arrow-down").show(); + this.el.find("#openwebrx-rx-details-arrow-up").hide(); +} + +Header.prototype.open_rx_photo = function() { + this.rx_photo_state = 1; + this.el.find("#webrx-rx-photo-desc").animate({opacity: 1}); + this.el.find("#webrx-rx-photo-title").animate({opacity: 1}); + this.el.find('#webrx-top-photo-clip').animate({maxHeight: this.rx_photo_height}, {duration: 1000, easing: 'easeOutCubic'}); + this.el.find("#openwebrx-rx-details-arrow-down").hide(); + this.el.find("#openwebrx-rx-details-arrow-up").show(); +} + +Header.prototype.toggle_rx_photo = function(ev) { + if (ev && ev.target && ev.target.tagName == 'A') { + return; + } + if (this.rx_photo_state) { + this.close_rx_photo(); + } else { + this.open_rx_photo(); + } +}; + $.fn.header = function() { if (!this.data('header')) { this.data('header', new Header(this)); diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index fb5586d..a6771f8 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -31,68 +31,11 @@ var fft_compression = "none"; var fft_codec; var waterfall_setup_done = 0; var secondary_fft_size; -var rx_photo_state = 1; function e(what) { return document.getElementById(what); } -var rx_photo_height; - -function init_rx_photo() { - var clip = e("webrx-top-photo-clip"); - rx_photo_height = clip.clientHeight; - clip.style.maxHeight = rx_photo_height + "px"; - - $.extend($.easing, { - easeOutCubic:function(x) { - return 1 - Math.pow( 1 - x, 3 ); - } - }); - - window.setTimeout(function () { - $('#webrx-rx-photo-title').animate({opacity: 0}, 500); - }, 1000); - window.setTimeout(function () { - $('#webrx-rx-photo-desc').animate({opacity: 0}, 500); - }, 1500); - window.setTimeout(function () { - close_rx_photo() - }, 2500); - $('#webrx-top-container').find('.openwebrx-photo-trigger').click(toggle_rx_photo); -} - -var dont_toggle_rx_photo_flag = 0; - -function dont_toggle_rx_photo() { - dont_toggle_rx_photo_flag = 1; -} - -function toggle_rx_photo() { - if (dont_toggle_rx_photo_flag) { - dont_toggle_rx_photo_flag = 0; - return; - } - if (rx_photo_state) close_rx_photo(); - else open_rx_photo() -} - -function close_rx_photo() { - rx_photo_state = 0; - $('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'}); - e("openwebrx-rx-details-arrow-down").style.display = "block"; - e("openwebrx-rx-details-arrow-up").style.display = "none"; -} - -function open_rx_photo() { - rx_photo_state = 1; - e("webrx-rx-photo-desc").style.opacity = 1; - e("webrx-rx-photo-title").style.opacity = 1; - $('#webrx-top-photo-clip').animate({maxHeight: rx_photo_height}, {duration: 1000, easing: 'easeOutCubic'}); - e("openwebrx-rx-details-arrow-down").style.display = "none"; - e("openwebrx-rx-details-arrow-up").style.display = "block"; -} - function updateVolume() { audioEngine.setVolume(parseFloat(e("openwebrx-panel-volume").value) / 100); } @@ -1378,7 +1321,6 @@ function openwebrx_init() { } fft_codec = new ImaAdpcmCodec(); initProgressBars(); - init_rx_photo(); open_websocket(); secondary_demod_init(); digimodes_init(); From 59a7842c6d13e524868a56c0eba937e933786a48 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 9 May 2020 01:18:51 +0200 Subject: [PATCH 309/475] fix map info window popping up after close --- htdocs/map.js | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/htdocs/map.js b/htdocs/map.js index 339d1d5..0d447e2 100644 --- a/htdocs/map.js +++ b/htdocs/map.js @@ -135,7 +135,11 @@ if (expectedCallsign && expectedCallsign == update.callsign.trim()) { map.panTo(pos); showMarkerInfoWindow(update.callsign, pos); - delete(expectedCallsign); + expectedCallsign = false; + } + + if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign.trim()) { + showMarkerInfoWindow(infowindow.callsign, pos); } break; case 'locator': @@ -176,7 +180,11 @@ if (expectedLocator && expectedLocator == update.location.locator) { map.panTo(center); showLocatorInfoWindow(expectedLocator, center); - delete(expectedLocator); + expectedLocator = false; + } + + if (infowindow && infowindow.locator && infowindow.locator == update.location.locator) { + showLocatorInfoWindow(infowindow.locator, center); } break; } @@ -287,9 +295,21 @@ connect(); + var getInfoWindow = function() { + if (!infowindow) { + infowindow = new google.maps.InfoWindow(); + google.maps.event.addListener(infowindow, 'closeclick', function() { + delete infowindow.locator; + delete infowindow.callsign; + }); + } + return infowindow; + } + var infowindow; var showLocatorInfoWindow = function(locator, pos) { - if (!infowindow) infowindow = new google.maps.InfoWindow(); + var infowindow = getInfoWindow(); + infowindow.locator = locator; var inLocator = $.map(rectangles, function(r, callsign) { return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band} }).filter(function(d) { @@ -315,7 +335,8 @@ }; var showMarkerInfoWindow = function(callsign, pos) { - if (!infowindow) infowindow = new google.maps.InfoWindow(); + var infowindow = getInfoWindow(); + infowindow.callsign = callsign; var marker = markers[callsign]; var timestring = moment(marker.lastseen).fromNow(); var commentString = ""; From 56066460649c48e6715052311f1b1f405acb2272 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 00:03:14 +0200 Subject: [PATCH 310/475] implement basic support for radioberry --- owrx/feature.py | 9 +++++++++ owrx/source/radioberry.py | 6 ++++++ 2 files changed, 15 insertions(+) create mode 100644 owrx/source/radioberry.py diff --git a/owrx/feature.py b/owrx/feature.py index 17172a5..6d5b4fa 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -34,6 +34,7 @@ class FeatureDetector(object): "soapy_remote": ["soapy_connector", "soapy_remote"], "uhd": ["soapy_connector", "soapy_uhd"], "red_pitaya": ["soapy_connector", "soapy_red_pitaya"], + "radioberry": ["soapy_connector", "soapy_radioberry"], # optional features and their requirements "digital_voice_digiham": ["digiham", "sox"], "digital_voice_dsd": ["dsd", "sox", "digiham"], @@ -333,6 +334,14 @@ class FeatureDetector(object): """ return self._has_soapy_driver("redpitaya") + def has_soapy_radioberry(self): + """ + The Radioberry is a SDR hat for the Raspberry Pi. + + You can find more information, along with its SoapySDR module [here](https://github.com/pa3gsb/Radioberry-2.x). + """ + return self._has_soapy_driver("radioberry") + def has_dsd(self): """ The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version diff --git a/owrx/source/radioberry.py b/owrx/source/radioberry.py new file mode 100644 index 0000000..5bb95c7 --- /dev/null +++ b/owrx/source/radioberry.py @@ -0,0 +1,6 @@ +from .soapy import SoapyConnectorSource + + +class RadioberrySource(SoapyConnectorSource): + def getDriver(self): + return "radioberry" From eaa41c3256c811bff57b1e0f6887f073b0889f6e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 16:07:14 +0200 Subject: [PATCH 311/475] header is now collapsed by default; simpler javascript --- htdocs/css/openwebrx-header.css | 13 ++++++------- htdocs/include/header.include.html | 2 +- htdocs/lib/Header.js | 14 ++------------ 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css index c369fb1..09c1fed 100644 --- a/htdocs/css/openwebrx-header.css +++ b/htdocs/css/openwebrx-header.css @@ -2,6 +2,7 @@ { position: relative; z-index:1000; + background-color: #575757; } #webrx-top-photo @@ -13,7 +14,8 @@ #webrx-top-photo-clip { min-height: 67px; - max-height: 350px; + max-height: 67px; + height: 350px; overflow: hidden; position: relative; } @@ -101,18 +103,15 @@ cursor:pointer; position: absolute; left: 470px; - top: 51px; + top: 55px; } #openwebrx-rx-details-arrow a { margin: 0; padding: 0; -} - -#openwebrx-rx-details-arrow-down -{ - display:none; + line-height: 0; + display: block; } #openwebrx-main-buttons .button { diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html index aa2275a..09fafa2 100644 --- a/htdocs/include/header.include.html +++ b/htdocs/include/header.include.html @@ -9,7 +9,7 @@
- +
diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js index ebdc5b1..a81a682 100644 --- a/htdocs/lib/Header.js +++ b/htdocs/lib/Header.js @@ -17,10 +17,7 @@ Header.prototype.setDetails = function(details) { }; Header.prototype.init_rx_photo = function() { - var clip = this.el.find("#webrx-top-photo-clip")[0]; - this.rx_photo_height = clip.clientHeight; - clip.style.maxHeight = this.rx_photo_height + "px"; - this.rx_photo_state = 1; + this.rx_photo_state = 0; $.extend($.easing, { easeOutCubic:function(x) { @@ -28,13 +25,6 @@ Header.prototype.init_rx_photo = function() { } }); - window.setTimeout(function () { - $('#webrx-rx-photo-title').animate({opacity: 0}, 500); - }, 1000); - window.setTimeout(function () { - $('#webrx-rx-photo-desc').animate({opacity: 0}, 500); - }, 1500); - window.setTimeout(this.close_rx_photo.bind(this), 2500); $('#webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this)); }; @@ -51,7 +41,7 @@ Header.prototype.open_rx_photo = function() { this.rx_photo_state = 1; this.el.find("#webrx-rx-photo-desc").animate({opacity: 1}); this.el.find("#webrx-rx-photo-title").animate({opacity: 1}); - this.el.find('#webrx-top-photo-clip').animate({maxHeight: this.rx_photo_height}, {duration: 1000, easing: 'easeOutCubic'}); + this.el.find('#webrx-top-photo-clip').animate({maxHeight: 350}, {duration: 1000, easing: 'easeOutCubic'}); this.el.find("#openwebrx-rx-details-arrow-down").hide(); this.el.find("#openwebrx-rx-details-arrow-up").show(); } From 4971bee67c6249ca7de9960d5fd23487caddc11b Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 16:12:37 +0200 Subject: [PATCH 312/475] create a javascript profile for the map, too --- htdocs/map.html | 5 +---- owrx/controllers/assets.py | 6 ++++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/htdocs/map.html b/htdocs/map.html index dbfd13d..08e40b4 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -3,11 +3,8 @@ OpenWebRX Map - - - + - diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py index f0bf6ac..59a532f 100644 --- a/owrx/controllers/assets.py +++ b/owrx/controllers/assets.py @@ -81,6 +81,12 @@ class CompiledAssetsController(Controller): "lib/Js8Threads.js", "lib/Modes.js", ], + "map.js": [ + "lib/jquery-3.2.1.min.js", + "lib/chroma.min.js", + "lib/Header.js", + "map.js", + ], } def indexAction(self): From 2c1ec7df74beb3a41ee9fa2c50120bdfe9f1fd7c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 16:23:05 +0200 Subject: [PATCH 313/475] make the header work on all pages --- htdocs/css/admin.css | 5 ----- htdocs/css/features.css | 5 ----- htdocs/css/login.css | 6 ------ htdocs/features.html | 1 + htdocs/generalsettings.html | 1 + htdocs/lib/Header.js | 6 +++++- htdocs/login.html | 2 ++ htdocs/openwebrx.js | 1 - htdocs/sdrsettings.html | 2 +- htdocs/settings.html | 2 +- 10 files changed, 11 insertions(+), 20 deletions(-) diff --git a/htdocs/css/admin.css b/htdocs/css/admin.css index fa2b020..4dd3f80 100644 --- a/htdocs/css/admin.css +++ b/htdocs/css/admin.css @@ -1,11 +1,6 @@ @import url("openwebrx-header.css"); @import url("openwebrx-globals.css"); -/* expandable photo not implemented in admin area page */ -#webrx-top-photo-clip { - max-height: 67px; -} - body { background-color: #2e2e2e; color: #DDD; diff --git a/htdocs/css/features.css b/htdocs/css/features.css index 8131da6..d52cae9 100644 --- a/htdocs/css/features.css +++ b/htdocs/css/features.css @@ -1,11 +1,6 @@ @import url("openwebrx-header.css"); @import url("openwebrx-globals.css"); -/* expandable photo not implemented on features page */ -#webrx-top-photo-clip { - max-height: 67px; -} - h1 { text-align: center; margin: 50px 0; diff --git a/htdocs/css/login.css b/htdocs/css/login.css index 84a2430..c50341e 100644 --- a/htdocs/css/login.css +++ b/htdocs/css/login.css @@ -1,12 +1,6 @@ @import url("openwebrx-header.css"); @import url("openwebrx-globals.css"); -/* expandable photo not implemented on login page */ -#webrx-top-photo-clip { - max-height: 67px; -} - - body { background-color: #2e2e2e; } diff --git a/htdocs/features.html b/htdocs/features.html index 8ddc941..2a97cd1 100644 --- a/htdocs/features.html +++ b/htdocs/features.html @@ -6,6 +6,7 @@ + ${header} diff --git a/htdocs/generalsettings.html b/htdocs/generalsettings.html index edd52e7..ae0656c 100644 --- a/htdocs/generalsettings.html +++ b/htdocs/generalsettings.html @@ -6,6 +6,7 @@ + diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js index a81a682..d1c6a30 100644 --- a/htdocs/lib/Header.js +++ b/htdocs/lib/Header.js @@ -62,4 +62,8 @@ $.fn.header = function() { this.data('header', new Header(this)); } return this.data('header'); -}; \ No newline at end of file +}; + +$(function(){ + $('#webrx-top-container').header(); +}); diff --git a/htdocs/login.html b/htdocs/login.html index b86efd9..9cbeacc 100644 --- a/htdocs/login.html +++ b/htdocs/login.html @@ -5,6 +5,8 @@ + + diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index a6771f8..dadd7e3 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1328,7 +1328,6 @@ function openwebrx_init() { $('.webrx-mouse-freq').frequencyDisplay(); $('#openwebrx-panel-receiver').demodulatorPanel(); window.addEventListener("resize", openwebrx_resize); - $('#webrx-top-container').header(); bookmarks = new BookmarkBar(); initSliders(); } diff --git a/htdocs/sdrsettings.html b/htdocs/sdrsettings.html index 51de0f0..08664fe 100644 --- a/htdocs/sdrsettings.html +++ b/htdocs/sdrsettings.html @@ -6,7 +6,7 @@ - + diff --git a/htdocs/settings.html b/htdocs/settings.html index 00fbe9e..6e6bd89 100644 --- a/htdocs/settings.html +++ b/htdocs/settings.html @@ -6,7 +6,7 @@ - + From a37e5ac93f7a966a5b3f184d05441dd77e1fb8c4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 16:07:14 +0200 Subject: [PATCH 314/475] header is now collapsed by default; simpler javascript --- htdocs/css/openwebrx-header.css | 13 ++++++------- htdocs/include/header.include.html | 2 +- htdocs/lib/Header.js | 14 ++------------ 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css index c369fb1..09c1fed 100644 --- a/htdocs/css/openwebrx-header.css +++ b/htdocs/css/openwebrx-header.css @@ -2,6 +2,7 @@ { position: relative; z-index:1000; + background-color: #575757; } #webrx-top-photo @@ -13,7 +14,8 @@ #webrx-top-photo-clip { min-height: 67px; - max-height: 350px; + max-height: 67px; + height: 350px; overflow: hidden; position: relative; } @@ -101,18 +103,15 @@ cursor:pointer; position: absolute; left: 470px; - top: 51px; + top: 55px; } #openwebrx-rx-details-arrow a { margin: 0; padding: 0; -} - -#openwebrx-rx-details-arrow-down -{ - display:none; + line-height: 0; + display: block; } #openwebrx-main-buttons .button { diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html index aa2275a..09fafa2 100644 --- a/htdocs/include/header.include.html +++ b/htdocs/include/header.include.html @@ -9,7 +9,7 @@
- +
diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js index ebdc5b1..a81a682 100644 --- a/htdocs/lib/Header.js +++ b/htdocs/lib/Header.js @@ -17,10 +17,7 @@ Header.prototype.setDetails = function(details) { }; Header.prototype.init_rx_photo = function() { - var clip = this.el.find("#webrx-top-photo-clip")[0]; - this.rx_photo_height = clip.clientHeight; - clip.style.maxHeight = this.rx_photo_height + "px"; - this.rx_photo_state = 1; + this.rx_photo_state = 0; $.extend($.easing, { easeOutCubic:function(x) { @@ -28,13 +25,6 @@ Header.prototype.init_rx_photo = function() { } }); - window.setTimeout(function () { - $('#webrx-rx-photo-title').animate({opacity: 0}, 500); - }, 1000); - window.setTimeout(function () { - $('#webrx-rx-photo-desc').animate({opacity: 0}, 500); - }, 1500); - window.setTimeout(this.close_rx_photo.bind(this), 2500); $('#webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this)); }; @@ -51,7 +41,7 @@ Header.prototype.open_rx_photo = function() { this.rx_photo_state = 1; this.el.find("#webrx-rx-photo-desc").animate({opacity: 1}); this.el.find("#webrx-rx-photo-title").animate({opacity: 1}); - this.el.find('#webrx-top-photo-clip').animate({maxHeight: this.rx_photo_height}, {duration: 1000, easing: 'easeOutCubic'}); + this.el.find('#webrx-top-photo-clip').animate({maxHeight: 350}, {duration: 1000, easing: 'easeOutCubic'}); this.el.find("#openwebrx-rx-details-arrow-down").hide(); this.el.find("#openwebrx-rx-details-arrow-up").show(); } From 508ea2cf9627479dcd8fe8100ce50315adc204e3 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 16:12:37 +0200 Subject: [PATCH 315/475] create a javascript profile for the map, too --- htdocs/map.html | 5 +---- owrx/controllers/assets.py | 6 ++++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/htdocs/map.html b/htdocs/map.html index dbfd13d..08e40b4 100644 --- a/htdocs/map.html +++ b/htdocs/map.html @@ -3,11 +3,8 @@ OpenWebRX Map - - - + - diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py index f0bf6ac..59a532f 100644 --- a/owrx/controllers/assets.py +++ b/owrx/controllers/assets.py @@ -81,6 +81,12 @@ class CompiledAssetsController(Controller): "lib/Js8Threads.js", "lib/Modes.js", ], + "map.js": [ + "lib/jquery-3.2.1.min.js", + "lib/chroma.min.js", + "lib/Header.js", + "map.js", + ], } def indexAction(self): From 813474b5d6b6e5678e2040f65fbd5cc3503af7c7 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 16:23:05 +0200 Subject: [PATCH 316/475] make the header work on all pages --- htdocs/css/admin.css | 5 ----- htdocs/css/features.css | 5 ----- htdocs/css/login.css | 6 ------ htdocs/features.html | 1 + htdocs/generalsettings.html | 1 + htdocs/lib/Header.js | 6 +++++- htdocs/login.html | 2 ++ htdocs/openwebrx.js | 1 - htdocs/sdrsettings.html | 2 +- htdocs/settings.html | 2 +- 10 files changed, 11 insertions(+), 20 deletions(-) diff --git a/htdocs/css/admin.css b/htdocs/css/admin.css index fa2b020..4dd3f80 100644 --- a/htdocs/css/admin.css +++ b/htdocs/css/admin.css @@ -1,11 +1,6 @@ @import url("openwebrx-header.css"); @import url("openwebrx-globals.css"); -/* expandable photo not implemented in admin area page */ -#webrx-top-photo-clip { - max-height: 67px; -} - body { background-color: #2e2e2e; color: #DDD; diff --git a/htdocs/css/features.css b/htdocs/css/features.css index 8131da6..d52cae9 100644 --- a/htdocs/css/features.css +++ b/htdocs/css/features.css @@ -1,11 +1,6 @@ @import url("openwebrx-header.css"); @import url("openwebrx-globals.css"); -/* expandable photo not implemented on features page */ -#webrx-top-photo-clip { - max-height: 67px; -} - h1 { text-align: center; margin: 50px 0; diff --git a/htdocs/css/login.css b/htdocs/css/login.css index 84a2430..c50341e 100644 --- a/htdocs/css/login.css +++ b/htdocs/css/login.css @@ -1,12 +1,6 @@ @import url("openwebrx-header.css"); @import url("openwebrx-globals.css"); -/* expandable photo not implemented on login page */ -#webrx-top-photo-clip { - max-height: 67px; -} - - body { background-color: #2e2e2e; } diff --git a/htdocs/features.html b/htdocs/features.html index 8ddc941..2a97cd1 100644 --- a/htdocs/features.html +++ b/htdocs/features.html @@ -6,6 +6,7 @@ + ${header} diff --git a/htdocs/generalsettings.html b/htdocs/generalsettings.html index edd52e7..ae0656c 100644 --- a/htdocs/generalsettings.html +++ b/htdocs/generalsettings.html @@ -6,6 +6,7 @@ + diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js index a81a682..d1c6a30 100644 --- a/htdocs/lib/Header.js +++ b/htdocs/lib/Header.js @@ -62,4 +62,8 @@ $.fn.header = function() { this.data('header', new Header(this)); } return this.data('header'); -}; \ No newline at end of file +}; + +$(function(){ + $('#webrx-top-container').header(); +}); diff --git a/htdocs/login.html b/htdocs/login.html index b86efd9..9cbeacc 100644 --- a/htdocs/login.html +++ b/htdocs/login.html @@ -5,6 +5,8 @@ + + diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index a6771f8..dadd7e3 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -1328,7 +1328,6 @@ function openwebrx_init() { $('.webrx-mouse-freq').frequencyDisplay(); $('#openwebrx-panel-receiver').demodulatorPanel(); window.addEventListener("resize", openwebrx_resize); - $('#webrx-top-container').header(); bookmarks = new BookmarkBar(); initSliders(); } diff --git a/htdocs/sdrsettings.html b/htdocs/sdrsettings.html index 51de0f0..08664fe 100644 --- a/htdocs/sdrsettings.html +++ b/htdocs/sdrsettings.html @@ -6,7 +6,7 @@ - + diff --git a/htdocs/settings.html b/htdocs/settings.html index 00fbe9e..6e6bd89 100644 --- a/htdocs/settings.html +++ b/htdocs/settings.html @@ -6,7 +6,7 @@ - + From 11cf2a96e240e4d2f68694b0d95f87cd5a339010 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 17:12:42 +0200 Subject: [PATCH 317/475] create a receiver details route for use in the header --- owrx/controllers/api.py | 13 +++++++++++++ owrx/http.py | 1 + 2 files changed, 14 insertions(+) diff --git a/owrx/controllers/api.py b/owrx/controllers/api.py index 4e7a966..60bb74f 100644 --- a/owrx/controllers/api.py +++ b/owrx/controllers/api.py @@ -1,5 +1,6 @@ from . import Controller from owrx.feature import FeatureDetector +from owrx.config import Config import json @@ -7,3 +8,15 @@ class ApiController(Controller): def indexAction(self): data = json.dumps(FeatureDetector().feature_report()) self.send_response(data, content_type="application/json") + + def receiverDetails(self): + receiver_details = Config.get().filter( + "receiver_name", + "receiver_location", + "receiver_asl", + "receiver_gps", + "photo_title", + "photo_desc", + ) + data = json.dumps(receiver_details.__dict__()) + self.send_response(data, content_type="application/json") diff --git a/owrx/http.py b/owrx/http.py index 39e2bdf..8381646 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -101,6 +101,7 @@ class Router(object): StaticRoute("/map", MapController), StaticRoute("/features", FeatureController), StaticRoute("/api/features", ApiController), + StaticRoute("/api/receiverdetails", ApiController, options={"action": "receiverDetails"}), StaticRoute("/metrics", MetricsController), StaticRoute("/settings", SettingsController), StaticRoute("/generalsettings", GeneralSettingsController), From 8df885b727b15bb25b3c9b42744e1c0a8a248811 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 17:27:46 +0200 Subject: [PATCH 318/475] download receiver details via rest api --- htdocs/lib/Header.js | 8 ++++++++ owrx/connection.py | 12 ++---------- owrx/controllers/api.py | 11 ++--------- owrx/details.py | 21 +++++++++++++++++++++ 4 files changed, 33 insertions(+), 19 deletions(-) create mode 100644 owrx/details.py diff --git a/htdocs/lib/Header.js b/htdocs/lib/Header.js index d1c6a30..cced6b6 100644 --- a/htdocs/lib/Header.js +++ b/htdocs/lib/Header.js @@ -6,6 +6,7 @@ function Header(el) { }); this.init_rx_photo(); + this.download_details(); }; Header.prototype.setDetails = function(details) { @@ -57,6 +58,13 @@ Header.prototype.toggle_rx_photo = function(ev) { } }; +Header.prototype.download_details = function() { + var self = this; + $.ajax('api/receiverdetails').done(function(data){ + self.setDetails(data); + }); +}; + $.fn.header = function() { if (!this.data('header')) { this.data('header', new Header(this)); diff --git a/owrx/connection.py b/owrx/connection.py index 14620fe..e8f5172 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -1,4 +1,5 @@ from owrx.config import Config +from owrx.details import ReceiverDetails from owrx.dsp import DspManager from owrx.cpu import CpuUsageThread from owrx.sdr import SdrService @@ -9,7 +10,6 @@ from owrx.version import openwebrx_version from owrx.bands import Bandplan from owrx.bookmarks import Bookmarks from owrx.map import Map -from owrx.locator import Locator from owrx.property import PropertyStack from owrx.modes import Modes, DigitalMode from multiprocessing import Queue @@ -68,18 +68,10 @@ class OpenWebRxClient(Client, metaclass=ABCMeta): def __init__(self, conn): super().__init__(conn) - receiver_details = Config.get().filter( - "receiver_name", - "receiver_location", - "receiver_asl", - "receiver_gps", - "photo_title", - "photo_desc", - ) + receiver_details = ReceiverDetails() def send_receiver_info(*args): receiver_info = receiver_details.__dict__() - receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"]) self.write_receiver_details(receiver_info) # TODO unsubscribe diff --git a/owrx/controllers/api.py b/owrx/controllers/api.py index 60bb74f..4dcde14 100644 --- a/owrx/controllers/api.py +++ b/owrx/controllers/api.py @@ -1,6 +1,6 @@ from . import Controller from owrx.feature import FeatureDetector -from owrx.config import Config +from owrx.details import ReceiverDetails import json @@ -10,13 +10,6 @@ class ApiController(Controller): self.send_response(data, content_type="application/json") def receiverDetails(self): - receiver_details = Config.get().filter( - "receiver_name", - "receiver_location", - "receiver_asl", - "receiver_gps", - "photo_title", - "photo_desc", - ) + receiver_details = ReceiverDetails() data = json.dumps(receiver_details.__dict__()) self.send_response(data, content_type="application/json") diff --git a/owrx/details.py b/owrx/details.py new file mode 100644 index 0000000..5bc7253 --- /dev/null +++ b/owrx/details.py @@ -0,0 +1,21 @@ +from owrx.config import Config +from owrx.locator import Locator +from owrx.property import PropertyFilter + + +class ReceiverDetails(PropertyFilter): + def __init__(self): + super().__init__( + Config.get(), + "receiver_name", + "receiver_location", + "receiver_asl", + "receiver_gps", + "photo_title", + "photo_desc", + ) + + def __dict__(self): + receiver_info = super().__dict__() + receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"]) + return receiver_info From 9366d67218299adea181c292fe831fa73975d722 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 20:18:42 +0200 Subject: [PATCH 319/475] dynamic sdr device settings --- htdocs/settings.js | 62 +++++++++++++++++++++++++++++++++++- owrx/controllers/settings.py | 30 ++++++++++------- 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/htdocs/settings.js b/htdocs/settings.js index 89c3eea..c9cb97f 100644 --- a/htdocs/settings.js +++ b/htdocs/settings.js @@ -1,3 +1,61 @@ +function Input(name, value) { + this.name = name; + this.value = value; +}; + +Input.prototype.bootstrapify = function(input, label) { + input.addClass('form-control').addClass('form-control-sm'); + return [ + '
', + '', + '
', + input[0].outerHTML, + '
', + '
' + ].join(''); +}; + +function TextInput() { + Input.apply(this, arguments); +}; + +TextInput.prototype = new Input(); + +TextInput.prototype.render = function() { + return this.bootstrapify($('')); +} + +Input.mappings = { + "name": TextInput +}; + +function SdrDevice(el) { + this.el = el; + this.data = JSON.parse(decodeURIComponent(el.data('config'))); + this.inputs = {}; + this.render(); +}; + +SdrDevice.prototype.render = function() { + var self = this; + $.each(this.data, function(key, value) { + var inputClass = Input.mappings[key] || TextInput; + var input = new inputClass(key, value); + self.inputs[key] = input; + self.el.append(input.render()) + }); +}; + +$.fn.sdrdevice = function() { + return this.map(function(){ + var el = $(this); + if (!el.data('sdrdevice')) { + el.data('sdrdevice', new SdrDevice(el)); + } + return el.data('sdrdevice'); + }); +}; + $(function(){ $(".map-input").each(function(el) { var $el = $(this); @@ -19,5 +77,7 @@ $(function(){ $lon.val(pos.lng); }); }); - }) + }); + + console.info($(".sdrdevice").sdrdevice()); }); \ No newline at end of file diff --git a/owrx/controllers/settings.py b/owrx/controllers/settings.py index 7a219e3..1eaceed 100644 --- a/owrx/controllers/settings.py +++ b/owrx/controllers/settings.py @@ -13,6 +13,8 @@ from owrx.form import ( ServicesCheckboxInput, Js8ProfileCheckboxInput, ) +from urllib.parse import quote +import json import logging logger = logging.getLogger(__name__) @@ -55,18 +57,24 @@ class SdrSettingsController(AdminController): return variables def render_devices(self): - def render_devicde(device_id, config): - return """ -
-
- {device_name} -
-
- device settings go here -
+ return "".join(self.render_device(key, value) for key, value in Config.get()["sdrs"].items()) + + def render_device(self, device_id, config): + return """ +
+
+ {device_name}
- """.format(device_name=config["name"]) - return "".join(render_devicde(key, value) for key, value in Config.get()["sdrs"].items()) +
+ {form} +
+
+ """.format(device_name=config["name"], form=self.render_form(device_id, config)) + + def render_form(self, device_id, config): + return """ + + """.format(device_id=device_id, formdata=quote(json.dumps(config))) def indexAction(self): self.serve_template("sdrsettings.html", **self.template_variables()) From 96b1de18560cf0b8c03e7d241baf0753ccb800ed Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 20:34:34 +0200 Subject: [PATCH 320/475] register different input types --- htdocs/settings.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/htdocs/settings.js b/htdocs/settings.js index c9cb97f..732e795 100644 --- a/htdocs/settings.js +++ b/htdocs/settings.js @@ -25,8 +25,42 @@ TextInput.prototype.render = function() { return this.bootstrapify($('')); } +function NumberInput() { + Input.apply(this, arguments); +}; + +NumberInput.prototype = new Input(); + +NumberInput.prototype.render = function() { + return this.bootstrapify($('')); +}; + +function ProfileInput() { + Input.apply(this, arguments); +}; + +ProfileInput.prototype = new Input(); + +ProfileInput.prototype.render = function() { + return $('

Profiles

'); +}; + +function SchedulerInput() { + Input.apply(this, arguments); +}; + +SchedulerInput.prototype = new Input(); + +SchedulerInput.prototype.render = function() { + return $('

Scheduler

'); +}; + Input.mappings = { - "name": TextInput + "name": TextInput, + "type": TextInput, + "ppm": NumberInput, + "profiles": ProfileInput, + "scheduler": SchedulerInput }; function SdrDevice(el) { From b0f7fd5d00b7c75c7bce25fe9985fa4761ca702f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 10 May 2020 22:42:09 +0200 Subject: [PATCH 321/475] ability to add more config keys --- htdocs/settings.js | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/htdocs/settings.js b/htdocs/settings.js index 732e795..55abf45 100644 --- a/htdocs/settings.js +++ b/htdocs/settings.js @@ -68,16 +68,44 @@ function SdrDevice(el) { this.data = JSON.parse(decodeURIComponent(el.data('config'))); this.inputs = {}; this.render(); + + var self = this; + el.on('click', '.fieldselector .btn', function() { + var key = el.find('.fieldselector select').val(); + self.data[key] = false; + self.render(); + }); }; SdrDevice.prototype.render = function() { var self = this; + self.el.empty(); $.each(this.data, function(key, value) { var inputClass = Input.mappings[key] || TextInput; var input = new inputClass(key, value); self.inputs[key] = input; - self.el.append(input.render()) + self.el.append(input.render()); }); + self.el.append(this.renderFieldSelector()); +}; + +SdrDevice.prototype.renderFieldSelector = function() { + var self = this; + return '
' + + '

Add new configuration options

' + + '
' + + '
' + + '
' + + '
Add to config
' + + '
' + + '
' + + '

'; }; $.fn.sdrdevice = function() { @@ -113,5 +141,5 @@ $(function(){ }); }); - console.info($(".sdrdevice").sdrdevice()); + $(".sdrdevice").sdrdevice(); }); \ No newline at end of file From db7b4f195e8ab90382455531e2a744a64c908ede Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 11 May 2020 15:04:24 +0200 Subject: [PATCH 322/475] fix for offset_freq when demodulator is exactly on center_freq --- htdocs/lib/DemodulatorPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js index bfd3619..439a8e5 100644 --- a/htdocs/lib/DemodulatorPanel.js +++ b/htdocs/lib/DemodulatorPanel.js @@ -222,7 +222,7 @@ DemodulatorPanel.prototype.transformHashParams = function(params) { var ret = { mod: params.secondary_mod || params.mod }; - if (params.offset_frequency) ret.offset_frequency = params.offset_frequency; + if (typeof(params.offset_frequency) !== 'undefined') ret.offset_frequency = params.offset_frequency; if (typeof(params.sql) !== 'undefined') ret.squelch_level = parseInt(params.sql); return ret; }; From a94209a2bc69d59929a810aa62ebe19a6e452beb Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 11 May 2020 20:31:21 +0200 Subject: [PATCH 323/475] apply some alt tags to images --- htdocs/include/header.include.html | 14 +++++++------- owrx/controllers/template.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html index 09fafa2..d4a2b53 100644 --- a/htdocs/include/header.include.html +++ b/htdocs/include/header.include.html @@ -1,9 +1,9 @@
- + Receiver panorama
- - + + Receiver avatar
@@ -13,10 +13,10 @@
-

Status
-

Log
-

Receiver
-
Map
+
Status
Status
+
Log
Log
+
Receiver
Receiver
+ Map
Map
${settingslink}
diff --git a/owrx/controllers/template.py b/owrx/controllers/template.py index 23c1d17..3d57861 100644 --- a/owrx/controllers/template.py +++ b/owrx/controllers/template.py @@ -23,7 +23,7 @@ class WebpageController(TemplateController): settingslink = "" pm = Config.get() if "webadmin_enabled" in pm and pm["webadmin_enabled"]: - settingslink = """
Settings
""" + settingslink = """Settings
Settings
""" header = self.render_template("include/header.include.html", settingslink=settingslink) return {"header": header} From 2483398b0f0eac6f018ffe748700b25631e311c4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 11 May 2020 23:20:03 +0200 Subject: [PATCH 324/475] clean up .wav files on exception, refs #107 --- owrx/audio.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/owrx/audio.py b/owrx/audio.py index aa8b5c0..a8c2880 100644 --- a/owrx/audio.py +++ b/owrx/audio.py @@ -25,6 +25,12 @@ class QueueJob(object): def run(self): self.decoder.decode(self) + def unlink(self): + try: + os.unlink(self.file) + except FileNotFoundError: + pass + class QueueWorker(threading.Thread): def __init__(self, queue): @@ -40,6 +46,9 @@ class QueueWorker(threading.Thread): except Exception: logger.exception("failed to decode job") self.queue.onError() + finally: + job.unlink() + self.queue.task_done() @@ -159,11 +168,12 @@ class AudioWriter(object): self.switchingLock.release() file.close() + job = QueueJob(self, filename, self.dsp.get_operating_freq()) try: - DecoderQueue.getSharedInstance().put(QueueJob(self, filename, self.dsp.get_operating_freq())) + DecoderQueue.getSharedInstance().put(job) except Full: logger.warning("decoding queue overflow; dropping one file") - os.unlink(filename) + job.unlink() self._scheduleNextSwitch() def decode(self, job: QueueJob): @@ -183,7 +193,6 @@ class AudioWriter(object): except subprocess.TimeoutExpired: logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid) decoder.kill() - os.unlink(job.file) def start(self): (self.wavefilename, self.wavefile) = self.getWaveFile() From 7d41fc8b0646406a4e7c5dae88222c88b1eabdae Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 11 May 2020 23:45:44 +0200 Subject: [PATCH 325/475] pass temporary directory to services, too --- owrx/service/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py index d4c97df..b4070a1 100644 --- a/owrx/service/__init__.py +++ b/owrx/service/__init__.py @@ -265,6 +265,7 @@ class ServiceHandler(object): d.set_secondary_demodulator(mode) d.set_audio_compression("none") d.set_samp_rate(source.getProps()["samp_rate"]) + d.set_temporary_directory(Config.get()['temporary_directory']) d.set_service() d.start() return d From 10d630960860a7ef1a9b707bf36fffc7ed6148ea Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 May 2020 21:40:28 +0200 Subject: [PATCH 326/475] add rockprog as a dependency --- owrx/feature.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/owrx/feature.py b/owrx/feature.py index 17172a5..5f09ea6 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -29,7 +29,7 @@ class FeatureDetector(object): "airspy": ["soapy_connector", "soapy_airspy"], "airspyhf": ["soapy_connector", "soapy_airspyhf"], "lime_sdr": ["soapy_connector", "soapy_lime_sdr"], - "fifi_sdr": ["alsa"], + "fifi_sdr": ["alsa", "rockprog"], "pluto_sdr": ["soapy_connector", "soapy_pluto_sdr"], "soapy_remote": ["soapy_connector", "soapy_remote"], "uhd": ["soapy_connector", "soapy_uhd"], @@ -387,3 +387,11 @@ class FeatureDetector(object): on the Alsa library. It is available as a package for most Linux distributions. """ return self.command_is_runnable("arecord --help") + + def has_rockprog(self): + """ + The "rockprog" executable is required to send commands to your FiFiSDR. It needs to be installed separately. + + You can find instructions and downloads [here](https://o28.sischa.net/fifisdr/trac/wiki/De%3Arockprog). + """ + return self.command_is_runnable("rockprog") From e557d46c0dd865538492f2ed75a09fd70583dd3a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 May 2020 22:31:54 +0200 Subject: [PATCH 327/475] apply darkly theme --- htdocs/css/admin.css | 5 ----- htdocs/css/bootstrap.min.css | 12 ++++++++++++ htdocs/css/features.css | 4 ---- htdocs/css/login.css | 8 -------- htdocs/css/openwebrx-header.css | 9 +++++++++ htdocs/features.html | 2 +- htdocs/generalsettings.html | 2 +- htdocs/login.html | 4 ++-- htdocs/sdrsettings.html | 2 +- htdocs/settings.html | 2 +- 10 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 htdocs/css/bootstrap.min.css diff --git a/htdocs/css/admin.css b/htdocs/css/admin.css index 4dd3f80..b6ebcea 100644 --- a/htdocs/css/admin.css +++ b/htdocs/css/admin.css @@ -1,11 +1,6 @@ @import url("openwebrx-header.css"); @import url("openwebrx-globals.css"); -body { - background-color: #2e2e2e; - color: #DDD; -} - .buttons { text-align: right; } diff --git a/htdocs/css/bootstrap.min.css b/htdocs/css/bootstrap.min.css new file mode 100644 index 0000000..43d80a0 --- /dev/null +++ b/htdocs/css/bootstrap.min.css @@ -0,0 +1,12 @@ +/*! + * Bootswatch v4.5.0 + * Homepage: https://bootswatch.com + * Copyright 2012-2020 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*//*! + * Bootstrap v4.5.0 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic&display=swap");:root{--blue: #375a7f;--indigo: #6610f2;--purple: #6f42c1;--pink: #e83e8c;--red: #E74C3C;--orange: #fd7e14;--yellow: #F39C12;--green: #00bc8c;--teal: #20c997;--cyan: #3498DB;--white: #fff;--gray: #888;--gray-dark: #303030;--primary: #375a7f;--secondary: #444;--success: #00bc8c;--info: #3498DB;--warning: #F39C12;--danger: #E74C3C;--light: #adb5bd;--dark: #303030;--breakpoint-xs: 0;--breakpoint-sm: 576px;--breakpoint-md: 768px;--breakpoint-lg: 992px;--breakpoint-xl: 1200px;--font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::before,*::after{-webkit-box-sizing:border-box;box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-size:0.9375rem;font-weight:400;line-height:1.5;color:#fff;text-align:left;background-color:#222}[tabindex="-1"]:focus:not(:focus-visible){outline:0 !important}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#00bc8c;text-decoration:none;background-color:transparent}a:hover{color:#007053;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:0.75rem;padding-bottom:0.75rem;color:#888;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:0.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role="button"]{cursor:pointer}select{word-wrap:normal}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:0.5rem;font-weight:500;line-height:1.2}h1,.h1{font-size:3rem}h2,.h2{font-size:2.5rem}h3,.h3{font-size:2rem}h4,.h4{font-size:1.40625rem}h5,.h5{font-size:1.171875rem}h6,.h6{font-size:0.9375rem}.lead{font-size:1.171875rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,0.1)}small,.small{font-size:80%;font-weight:400}mark,.mark{padding:0.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:0.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.171875rem}.blockquote-footer{display:block;font-size:80%;color:#888}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:0.25rem;background-color:#222;border:1px solid #dee2e6;border-radius:0.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:0.5rem;line-height:1}.figure-caption{font-size:90%;color:#888}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:0.2rem 0.4rem;font-size:87.5%;color:#fff;background-color:#222;border-radius:0.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:inherit}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}.container-fluid,.container-sm,.container-md,.container-lg,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container,.container-sm{max-width:540px}}@media (min-width: 768px){.container,.container-sm,.container-md{max-width:720px}}@media (min-width: 992px){.container,.container-sm,.container-md,.container-lg{max-width:960px}}@media (min-width: 1200px){.container,.container-sm,.container-md,.container-lg,.container-xl{max-width:1140px}}.row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*="col-"]{padding-right:0;padding-left:0}.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col,.col-auto,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm,.col-sm-auto,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-md,.col-md-auto,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg,.col-lg-auto,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-5{margin-left:41.6666666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.3333333333%}.offset-8{margin-left:66.6666666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.3333333333%}.offset-11{margin-left:91.6666666667%}@media (min-width: 576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-sm-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-sm-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-sm-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-sm-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-sm-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-sm-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-sm-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-sm-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-sm-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-sm-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-sm-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-sm-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-sm-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-sm-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-sm-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-sm-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-sm-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-sm-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-sm-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-sm-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-sm-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-sm-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-sm-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.3333333333%}.offset-sm-2{margin-left:16.6666666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.3333333333%}.offset-sm-5{margin-left:41.6666666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.3333333333%}.offset-sm-8{margin-left:66.6666666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.3333333333%}.offset-sm-11{margin-left:91.6666666667%}}@media (min-width: 768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-md-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-md-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-md-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-md-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-md-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-md-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-md-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-md-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-md-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-md-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-md-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-md-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-md-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-md-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-md-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-md-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-md-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-md-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-md-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-md-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-md-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-md-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-md-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.3333333333%}.offset-md-2{margin-left:16.6666666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.3333333333%}.offset-md-5{margin-left:41.6666666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.3333333333%}.offset-md-8{margin-left:66.6666666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.3333333333%}.offset-md-11{margin-left:91.6666666667%}}@media (min-width: 992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-lg-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-lg-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-lg-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-lg-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-lg-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-lg-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-lg-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-lg-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-lg-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-lg-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-lg-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-lg-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-lg-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-lg-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-lg-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-lg-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-lg-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-lg-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-lg-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-lg-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-lg-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-lg-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-lg-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.3333333333%}.offset-lg-2{margin-left:16.6666666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.3333333333%}.offset-lg-5{margin-left:41.6666666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.3333333333%}.offset-lg-8{margin-left:66.6666666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.3333333333%}.offset-lg-11{margin-left:91.6666666667%}}@media (min-width: 1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-xl-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-xl-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-xl-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-xl-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-xl-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-xl-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-xl-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-xl-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-xl-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-xl-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-xl-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-xl-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-xl-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-xl-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-xl-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-xl-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-xl-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-xl-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-xl-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-xl-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-xl-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-xl-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-xl-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.3333333333%}.offset-xl-2{margin-left:16.6666666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.3333333333%}.offset-xl-5{margin-left:41.6666666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.3333333333%}.offset-xl-8{margin-left:66.6666666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.3333333333%}.offset-xl-11{margin-left:91.6666666667%}}.table{width:100%;margin-bottom:1rem;color:#fff}.table th,.table td{padding:0.75rem;vertical-align:top;border-top:1px solid #444}.table thead th{vertical-align:bottom;border-bottom:2px solid #444}.table tbody+tbody{border-top:2px solid #444}.table-sm th,.table-sm td{padding:0.3rem}.table-bordered{border:1px solid #444}.table-bordered th,.table-bordered td{border:1px solid #444}.table-bordered thead th,.table-bordered thead td{border-bottom-width:2px}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody+tbody{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:#303030}.table-hover tbody tr:hover{color:#fff;background-color:rgba(0,0,0,0.075)}.table-primary,.table-primary>th,.table-primary>td{background-color:#c7d1db}.table-primary th,.table-primary td,.table-primary thead th,.table-primary tbody+tbody{border-color:#97a9bc}.table-hover .table-primary:hover{background-color:#b7c4d1}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#b7c4d1}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#cbcbcb}.table-secondary th,.table-secondary td,.table-secondary thead th,.table-secondary tbody+tbody{border-color:#9e9e9e}.table-hover .table-secondary:hover{background-color:#bebebe}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#bebebe}.table-success,.table-success>th,.table-success>td{background-color:#b8ecdf}.table-success th,.table-success td,.table-success thead th,.table-success tbody+tbody{border-color:#7adcc3}.table-hover .table-success:hover{background-color:#a4e7d6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#a4e7d6}.table-info,.table-info>th,.table-info>td{background-color:#c6e2f5}.table-info th,.table-info td,.table-info thead th,.table-info tbody+tbody{border-color:#95c9ec}.table-hover .table-info:hover{background-color:#b0d7f1}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#b0d7f1}.table-warning,.table-warning>th,.table-warning>td{background-color:#fce3bd}.table-warning th,.table-warning td,.table-warning thead th,.table-warning tbody+tbody{border-color:#f9cc84}.table-hover .table-warning:hover{background-color:#fbd9a5}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#fbd9a5}.table-danger,.table-danger>th,.table-danger>td{background-color:#f8cdc8}.table-danger th,.table-danger td,.table-danger thead th,.table-danger tbody+tbody{border-color:#f3a29a}.table-hover .table-danger:hover{background-color:#f5b8b1}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f5b8b1}.table-light,.table-light>th,.table-light>td{background-color:#e8eaed}.table-light th,.table-light td,.table-light thead th,.table-light tbody+tbody{border-color:#d4d9dd}.table-hover .table-light:hover{background-color:#dadde2}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#dadde2}.table-dark,.table-dark>th,.table-dark>td{background-color:#c5c5c5}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#939393}.table-hover .table-dark:hover{background-color:#b8b8b8}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b8b8b8}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,0.075)}.table .thead-dark th{color:#fff;background-color:#303030;border-color:#434343}.table .thead-light th{color:#444;background-color:#ebebeb;border-color:#444}.table-dark{color:#fff;background-color:#303030}.table-dark th,.table-dark td,.table-dark thead th{border-color:#434343}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,0.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#444;background-color:#fff;background-clip:padding-box;border:1px solid #222;border-radius:0.25rem;-webkit-transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{-webkit-transition:none;transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #444}.form-control:focus{color:#444;background-color:#fff;border-color:#739ac2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.form-control::-webkit-input-placeholder{color:#888;opacity:1}.form-control::-ms-input-placeholder{color:#888;opacity:1}.form-control::placeholder{color:#888;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#ebebeb;opacity:1}input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#444;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.171875rem;line-height:1.5}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.8203125rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:0.375rem 0;margin-bottom:0;font-size:0.9375rem;line-height:1.5;color:#fff;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + 0.5rem + 2px);padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}select.form-control[size],select.form-control[multiple]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:0.25rem}.form-row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*="col-"]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:0.3rem;margin-left:-1.25rem}.form-check-input[disabled] ~ .form-check-label,.form-check-input:disabled ~ .form-check-label{color:#888}.form-check-label{margin-bottom:0}.form-check-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:0.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:0.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#00bc8c}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:.1rem;font-size:0.8203125rem;line-height:1.5;color:#fff;background-color:rgba(0,188,140,0.9);border-radius:0.25rem}.was-validated :valid ~ .valid-feedback,.was-validated :valid ~ .valid-tooltip,.is-valid ~ .valid-feedback,.is-valid ~ .valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#00bc8c;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .custom-select:valid,.custom-select.is-valid{border-color:#00bc8c;padding-right:calc(0.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated .form-check-input:valid ~ .form-check-label,.form-check-input.is-valid ~ .form-check-label{color:#00bc8c}.was-validated .form-check-input:valid ~ .valid-feedback,.was-validated .form-check-input:valid ~ .valid-tooltip,.form-check-input.is-valid ~ .valid-feedback,.form-check-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid ~ .custom-control-label,.custom-control-input.is-valid ~ .custom-control-label{color:#00bc8c}.was-validated .custom-control-input:valid ~ .custom-control-label::before,.custom-control-input.is-valid ~ .custom-control-label::before{border-color:#00bc8c}.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,.custom-control-input.is-valid:checked ~ .custom-control-label::before{border-color:#00efb2;background-color:#00efb2}.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,.custom-control-input.is-valid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before{border-color:#00bc8c}.was-validated .custom-file-input:valid ~ .custom-file-label,.custom-file-input.is-valid ~ .custom-file-label{border-color:#00bc8c}.was-validated .custom-file-input:valid:focus ~ .custom-file-label,.custom-file-input.is-valid:focus ~ .custom-file-label{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.invalid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#E74C3C}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:.1rem;font-size:0.8203125rem;line-height:1.5;color:#fff;background-color:rgba(231,76,60,0.9);border-radius:0.25rem}.was-validated :invalid ~ .invalid-feedback,.was-validated :invalid ~ .invalid-tooltip,.is-invalid ~ .invalid-feedback,.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#E74C3C;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23E74C3C' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23E74C3C' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .custom-select:invalid,.custom-select.is-invalid{border-color:#E74C3C;padding-right:calc(0.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23E74C3C' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23E74C3C' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated .form-check-input:invalid ~ .form-check-label,.form-check-input.is-invalid ~ .form-check-label{color:#E74C3C}.was-validated .form-check-input:invalid ~ .invalid-feedback,.was-validated .form-check-input:invalid ~ .invalid-tooltip,.form-check-input.is-invalid ~ .invalid-feedback,.form-check-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid ~ .custom-control-label,.custom-control-input.is-invalid ~ .custom-control-label{color:#E74C3C}.was-validated .custom-control-input:invalid ~ .custom-control-label::before,.custom-control-input.is-invalid ~ .custom-control-label::before{border-color:#E74C3C}.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,.custom-control-input.is-invalid:checked ~ .custom-control-label::before{border-color:#ed7669;background-color:#ed7669}.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,.custom-control-input.is-invalid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before{border-color:#E74C3C}.was-validated .custom-file-input:invalid ~ .custom-file-label,.custom-file-input.is-invalid ~ .custom-file-label{border-color:#E74C3C}.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,.custom-file-input.is-invalid:focus ~ .custom-file-label{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.form-inline{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width: 576px){.form-inline label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group,.form-inline .custom-select{width:auto}.form-inline .form-check{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:0.25rem;margin-left:0}.form-inline .custom-control{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#fff;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:0.375rem 0.75rem;font-size:0.9375rem;line-height:1.5;border-radius:0.25rem;-webkit-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{-webkit-transition:none;transition:none}}.btn:hover{color:#fff;text-decoration:none}.btn:focus,.btn.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.btn.disabled,.btn:disabled{opacity:0.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:hover{color:#fff;background-color:#2b4764;border-color:#28415b}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#2b4764;border-color:#28415b;-webkit-box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5);box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#28415b;border-color:#243a53}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5);box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5)}.btn-secondary{color:#fff;background-color:#444;border-color:#444}.btn-secondary:hover{color:#fff;background-color:#313131;border-color:#2b2a2a}.btn-secondary:focus,.btn-secondary.focus{color:#fff;background-color:#313131;border-color:#2b2a2a;-webkit-box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5);box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#444;border-color:#444}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#2b2a2a;border-color:#242424}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5);box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5)}.btn-success{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:hover{color:#fff;background-color:#009670;border-color:#008966}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#009670;border-color:#008966;-webkit-box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5);box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#008966;border-color:#007c5d}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show>.btn-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5);box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5)}.btn-info{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-info:hover{color:#fff;background-color:#2384c6;border-color:#217dbb}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#2384c6;border-color:#217dbb;-webkit-box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5);box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#217dbb;border-color:#1f76b0}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show>.btn-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5);box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5)}.btn-warning{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-warning:hover{color:#fff;background-color:#d4860b;border-color:#c87f0a}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#d4860b;border-color:#c87f0a;-webkit-box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5);box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#c87f0a;border-color:#bc770a}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5);box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5)}.btn-danger{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-danger:hover{color:#fff;background-color:#e12e1c;border-color:#d62c1a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#e12e1c;border-color:#d62c1a;-webkit-box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5);box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#d62c1a;border-color:#ca2a19}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5);box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5)}.btn-light{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-light:hover{color:#fff;background-color:#98a2ac;border-color:#919ca6}.btn-light:focus,.btn-light.focus{color:#fff;background-color:#98a2ac;border-color:#919ca6;-webkit-box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5);box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5)}.btn-light.disabled,.btn-light:disabled{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-light:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.show>.btn-light.dropdown-toggle{color:#fff;background-color:#919ca6;border-color:#8a95a1}.btn-light:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.show>.btn-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5);box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5)}.btn-dark{color:#fff;background-color:#303030;border-color:#303030}.btn-dark:hover{color:#fff;background-color:#1d1d1d;border-color:#171616}.btn-dark:focus,.btn-dark.focus{color:#fff;background-color:#1d1d1d;border-color:#171616;-webkit-box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5);box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#303030;border-color:#303030}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#171616;border-color:#101010}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5);box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5)}.btn-outline-primary{color:#375a7f;border-color:#375a7f}.btn-outline-primary:hover{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary:focus,.btn-outline-primary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#375a7f;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.btn-outline-secondary{color:#444;border-color:#444}.btn-outline-secondary:hover{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary:focus,.btn-outline-secondary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#444;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.btn-outline-success{color:#00bc8c;border-color:#00bc8c}.btn-outline-success:hover{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success:focus,.btn-outline-success.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#00bc8c;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.btn-outline-info{color:#3498DB;border-color:#3498DB}.btn-outline-info:hover{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-outline-info:focus,.btn-outline-info.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#3498DB;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.btn-outline-warning{color:#F39C12;border-color:#F39C12}.btn-outline-warning:hover{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-outline-warning:focus,.btn-outline-warning.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#F39C12;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.btn-outline-danger{color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:hover{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:focus,.btn-outline-danger.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#E74C3C;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.btn-outline-light{color:#adb5bd;border-color:#adb5bd}.btn-outline-light:hover{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-outline-light:focus,.btn-outline-light.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#adb5bd;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show>.btn-outline-light.dropdown-toggle{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.btn-outline-dark{color:#303030;border-color:#303030}.btn-outline-dark:hover{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-dark:focus,.btn-outline-dark.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#303030;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.btn-link{font-weight:400;color:#00bc8c;text-decoration:none}.btn-link:hover{color:#007053;text-decoration:underline}.btn-link:focus,.btn-link.focus{text-decoration:underline}.btn-link:disabled,.btn-link.disabled{color:#888;pointer-events:none}.btn-lg,.btn-group-lg>.btn{padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}.btn-sm,.btn-group-sm>.btn{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:0.5rem}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{-webkit-transition:opacity 0.15s linear;transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{-webkit-transition:none;transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;transition:height 0.35s ease}@media (prefers-reduced-motion: reduce){.collapsing{-webkit-transition:none;transition:none}}.dropup,.dropright,.dropdown,.dropleft{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid;border-right:0.3em solid transparent;border-bottom:0;border-left:0.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:0.5rem 0;margin:0.125rem 0 0;font-size:0.9375rem;color:#fff;text-align:left;list-style:none;background-color:#222;background-clip:padding-box;border:1px solid #444;border-radius:0.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:0.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0;border-right:0.3em solid transparent;border-bottom:0.3em solid;border-left:0.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:0.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0;border-bottom:0.3em solid transparent;border-left:0.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:0.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0.3em solid;border-bottom:0.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:0.5rem 0;overflow:hidden;border-top:1px solid #444}.dropdown-item{display:block;width:100%;padding:0.25rem 1.5rem;clear:both;font-weight:400;color:#fff;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-item:focus{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.disabled,.dropdown-item:disabled{color:#888;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:0.5rem 1.5rem;margin-bottom:0;font-size:0.8203125rem;color:#888;white-space:nowrap}.dropdown-item-text{display:block;padding:0.25rem 1.5rem;color:#fff}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover{z-index:1}.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:0.5625rem;padding-left:0.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:0.375rem;padding-left:0.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:0.75rem;padding-left:0.75rem}.btn-group-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type="radio"],.btn-group-toggle>.btn input[type="checkbox"],.btn-group-toggle>.btn-group>.btn input[type="radio"],.btn-group-toggle>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-control-plaintext,.input-group>.custom-select,.input-group>.custom-file{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.form-control+.form-control,.input-group>.form-control+.custom-select,.input-group>.form-control+.custom-file,.input-group>.form-control-plaintext+.form-control,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.custom-file,.input-group>.custom-select+.form-control,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.custom-file,.input-group>.custom-file+.form-control,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.custom-file{margin-left:-1px}.input-group>.form-control:focus,.input-group>.custom-select:focus,.input-group>.custom-file .custom-file-input:focus ~ .custom-file-label{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.form-control:not(:last-child),.input-group>.custom-select:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.form-control:not(:first-child),.input-group>.custom-select:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-prepend,.input-group-append{display:-webkit-box;display:-ms-flexbox;display:flex}.input-group-prepend .btn,.input-group-append .btn{position:relative;z-index:2}.input-group-prepend .btn:focus,.input-group-append .btn:focus{z-index:3}.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.input-group-text,.input-group-append .input-group-text+.btn{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0.375rem 0.75rem;margin-bottom:0;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#adb5bd;text-align:center;white-space:nowrap;background-color:#444;border:1px solid #222;border-radius:0.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group-lg>.form-control:not(textarea),.input-group-lg>.custom-select{height:calc(1.5em + 1rem + 2px)}.input-group-lg>.form-control,.input-group-lg>.custom-select,.input-group-lg>.input-group-prepend>.input-group-text,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-append>.btn{padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}.input-group-sm>.form-control:not(textarea),.input-group-sm>.custom-select{height:calc(1.5em + 0.5rem + 2px)}.input-group-sm>.form-control,.input-group-sm>.custom-select,.input-group-sm>.input-group-prepend>.input-group-text,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-append>.btn{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text,.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.40625rem;padding-left:1.5rem}.custom-control-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.203125rem;opacity:0}.custom-control-input:checked ~ .custom-control-label::before{color:#fff;border-color:#375a7f;background-color:#375a7f}.custom-control-input:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-control-input:focus:not(:checked) ~ .custom-control-label::before{border-color:#739ac2}.custom-control-input:not(:disabled):active ~ .custom-control-label::before{color:#fff;background-color:#97b3d2;border-color:#97b3d2}.custom-control-input[disabled] ~ .custom-control-label,.custom-control-input:disabled ~ .custom-control-label{color:#888}.custom-control-input[disabled] ~ .custom-control-label::before,.custom-control-input:disabled ~ .custom-control-label::before{background-color:#ebebeb}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:0.203125rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:0.203125rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50% / 50% 50%}.custom-checkbox .custom-control-label::before{border-radius:0.25rem}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before{border-color:#375a7f;background-color:#375a7f}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:0.5rem}.custom-switch .custom-control-label::after{top:calc(0.203125rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:0.5rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-switch .custom-control-label::after{-webkit-transition:none;transition:none}}.custom-switch .custom-control-input:checked ~ .custom-control-label::after{background-color:#fff;-webkit-transform:translateX(0.75rem);transform:translateX(0.75rem)}.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 1.75rem 0.375rem 0.75rem;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#444;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;border:1px solid #222;border-radius:0.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#739ac2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-select:focus::-ms-value{color:#444;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:0.75rem;background-image:none}.custom-select:disabled{color:#888;background-color:#ebebeb}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #444}.custom-select-sm{height:calc(1.5em + 0.5rem + 2px);padding-top:0.25rem;padding-bottom:0.25rem;padding-left:0.5rem;font-size:0.8203125rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:0.5rem;padding-bottom:0.5rem;padding-left:1rem;font-size:1.171875rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + 0.75rem + 2px);margin:0;opacity:0}.custom-file-input:focus ~ .custom-file-label{border-color:#739ac2;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-file-input[disabled] ~ .custom-file-label,.custom-file-input:disabled ~ .custom-file-label{background-color:#ebebeb}.custom-file-input:lang(en) ~ .custom-file-label::after{content:"Browse"}.custom-file-input ~ .custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;font-weight:400;line-height:1.5;color:#adb5bd;background-color:#fff;border:1px solid #222;border-radius:0.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + 0.75rem);padding:0.375rem 0.75rem;line-height:1.5;color:#adb5bd;content:"Browse";background-color:#444;border-left:inherit;border-radius:0 0.25rem 0.25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:none}.custom-range:focus::-webkit-slider-thumb{-webkit-box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#97b3d2}.custom-range::-webkit-slider-runnable-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-moz-range-thumb{-webkit-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#97b3d2}.custom-range::-moz-range-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:0.2rem;margin-left:0.2rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-ms-thumb{-webkit-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#97b3d2}.custom-range::-ms-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:0.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-control-label::before,.custom-file-label,.custom-select{-webkit-transition:none;transition:none}}.nav{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:0.5rem 2rem}.nav-link:hover,.nav-link:focus{text-decoration:none}.nav-link.disabled{color:#adb5bd;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #444}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{border-color:#444 #444 transparent}.nav-tabs .nav-link.disabled{color:#adb5bd;background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:#fff;background-color:#222;border-color:#444 #444 transparent}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:0.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#375a7f}.nav-fill .nav-item{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-sm,.navbar .container-md,.navbar .container-lg,.navbar .container-xl{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:0.32421875rem;padding-bottom:0.32421875rem;margin-right:1rem;font-size:1.171875rem;line-height:inherit;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-nav{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:0.5rem;padding-bottom:0.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:0.25rem 0.75rem;font-size:1.171875rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:0.25rem}.navbar-toggler:hover,.navbar-toggler:focus{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width: 575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width: 767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width: 991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:#222}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus{color:#222}.navbar-light .navbar-nav .nav-link{color:rgba(34,34,34,0.7)}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus{color:#222}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,0.3)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active{color:#222}.navbar-light .navbar-toggler{color:rgba(34,34,34,0.7);border-color:rgba(34,34,34,0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2834, 34, 34, 0.7%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(34,34,34,0.7)}.navbar-light .navbar-text a{color:#222}.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus{color:#222}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,0.6)}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus{color:#fff}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,0.6);border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.6%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,0.6)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus{color:#fff}.card{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#303030;background-clip:border-box;border:1px solid rgba(0,0,0,0.125);border-radius:0.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(0.25rem - 1px);border-bottom-left-radius:calc(0.25rem - 1px)}.card-body{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:0.75rem}.card-subtitle{margin-top:-0.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:0.75rem 1.25rem;margin-bottom:0;background-color:#444;border-bottom:1px solid rgba(0,0,0,0.125)}.card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:0.75rem 1.25rem;background-color:#444;border-top:1px solid rgba(0,0,0,0.125)}.card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.card-header-tabs{margin-right:-0.625rem;margin-bottom:-0.75rem;margin-left:-0.625rem;border-bottom:0}.card-header-pills{margin-right:-0.625rem;margin-left:-0.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-top,.card-img-bottom{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(0.25rem - 1px);border-bottom-left-radius:calc(0.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width: 576px){.card-deck{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width: 576px){.card-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:0.75rem}@media (min-width: 576px){.card-columns{-webkit-column-count:3;column-count:3;-webkit-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#444;border-radius:0.25rem}.breadcrumb-item{display:-webkit-box;display:-ms-flexbox;display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:0.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:0.5rem;color:#888;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#888}.pagination{display:-webkit-box;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:0.25rem}.page-link{position:relative;display:block;padding:0.5rem 0.75rem;margin-left:0;line-height:1.25;color:#fff;background-color:#00bc8c;border:0 solid transparent}.page-link:hover{z-index:2;color:#fff;text-decoration:none;background-color:#00efb2;border-color:transparent}.page-link:focus{z-index:3;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem}.page-item:last-child .page-link{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#00efb2;border-color:transparent}.page-item.disabled .page-link{color:#fff;pointer-events:none;cursor:auto;background-color:#007053;border-color:transparent}.pagination-lg .page-link{padding:0.75rem 1.5rem;font-size:1.171875rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:0.3rem;border-bottom-left-radius:0.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:0.3rem;border-bottom-right-radius:0.3rem}.pagination-sm .page-link{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:0.2rem;border-bottom-left-radius:0.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:0.2rem;border-bottom-right-radius:0.2rem}.badge{display:inline-block;padding:0.25em 0.4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:0.25rem;-webkit-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.badge{-webkit-transition:none;transition:none}}a.badge:hover,a.badge:focus{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:0.6em;padding-left:0.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#375a7f}a.badge-primary:hover,a.badge-primary:focus{color:#fff;background-color:#28415b}a.badge-primary:focus,a.badge-primary.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.badge-secondary{color:#fff;background-color:#444}a.badge-secondary:hover,a.badge-secondary:focus{color:#fff;background-color:#2b2a2a}a.badge-secondary:focus,a.badge-secondary.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.badge-success{color:#fff;background-color:#00bc8c}a.badge-success:hover,a.badge-success:focus{color:#fff;background-color:#008966}a.badge-success:focus,a.badge-success.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.badge-info{color:#fff;background-color:#3498DB}a.badge-info:hover,a.badge-info:focus{color:#fff;background-color:#217dbb}a.badge-info:focus,a.badge-info.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.badge-warning{color:#fff;background-color:#F39C12}a.badge-warning:hover,a.badge-warning:focus{color:#fff;background-color:#c87f0a}a.badge-warning:focus,a.badge-warning.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.badge-danger{color:#fff;background-color:#E74C3C}a.badge-danger:hover,a.badge-danger:focus{color:#fff;background-color:#d62c1a}a.badge-danger:focus,a.badge-danger.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.badge-light{color:#222;background-color:#adb5bd}a.badge-light:hover,a.badge-light:focus{color:#222;background-color:#919ca6}a.badge-light:focus,a.badge-light.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.badge-dark{color:#fff;background-color:#303030}a.badge-dark:hover,a.badge-dark:focus{color:#fff;background-color:#171616}a.badge-dark:focus,a.badge-dark.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#303030;border-radius:0.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:0.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:0.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3.90625rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:0.75rem 1.25rem;color:inherit}.alert-primary{color:#1d2f42;background-color:#d7dee5;border-color:#c7d1db}.alert-primary hr{border-top-color:#b7c4d1}.alert-primary .alert-link{color:#0d161f}.alert-secondary{color:#232323;background-color:#dadada;border-color:#cbcbcb}.alert-secondary hr{border-top-color:#bebebe}.alert-secondary .alert-link{color:#0a0909}.alert-success{color:#006249;background-color:#ccf2e8;border-color:#b8ecdf}.alert-success hr{border-top-color:#a4e7d6}.alert-success .alert-link{color:#002f23}.alert-info{color:#1b4f72;background-color:#d6eaf8;border-color:#c6e2f5}.alert-info hr{border-top-color:#b0d7f1}.alert-info .alert-link{color:#113249}.alert-warning{color:#7e5109;background-color:#fdebd0;border-color:#fce3bd}.alert-warning hr{border-top-color:#fbd9a5}.alert-warning .alert-link{color:#4e3206}.alert-danger{color:#78281f;background-color:#fadbd8;border-color:#f8cdc8}.alert-danger hr{border-top-color:#f5b8b1}.alert-danger .alert-link{color:#4f1a15}.alert-light{color:#5a5e62;background-color:#eff0f2;border-color:#e8eaed}.alert-light hr{border-top-color:#dadde2}.alert-light .alert-link{color:#424547}.alert-dark{color:#191919;background-color:#d6d6d6;border-color:#c5c5c5}.alert-dark hr{border-top-color:#b8b8b8}.alert-dark .alert-link{color:black}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:0.703125rem;background-color:#444;border-radius:0.25rem}.progress-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#375a7f;-webkit-transition:width 0.6s ease;transition:width 0.6s ease}@media (prefers-reduced-motion: reduce){.progress-bar{-webkit-transition:none;transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion: reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-ms-flex:1;flex:1}.list-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:0.25rem}.list-group-item-action{width:100%;color:#444;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#444;text-decoration:none;background-color:#444}.list-group-item-action:active{color:#fff;background-color:#ebebeb}.list-group-item{position:relative;display:block;padding:0.75rem 1.25rem;background-color:#303030;border:1px solid #444}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#888;pointer-events:none;background-color:#303030}.list-group-item.active{z-index:2;color:#fff;background-color:#375a7f;border-color:#375a7f}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width: 576px){.list-group-horizontal-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 768px){.list-group-horizontal-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 992px){.list-group-horizontal-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 1200px){.list-group-horizontal-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#1d2f42;background-color:#c7d1db}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#1d2f42;background-color:#b7c4d1}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#1d2f42;border-color:#1d2f42}.list-group-item-secondary{color:#232323;background-color:#cbcbcb}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#232323;background-color:#bebebe}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#232323;border-color:#232323}.list-group-item-success{color:#006249;background-color:#b8ecdf}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#006249;background-color:#a4e7d6}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#006249;border-color:#006249}.list-group-item-info{color:#1b4f72;background-color:#c6e2f5}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#1b4f72;background-color:#b0d7f1}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#1b4f72;border-color:#1b4f72}.list-group-item-warning{color:#7e5109;background-color:#fce3bd}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#7e5109;background-color:#fbd9a5}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#7e5109;border-color:#7e5109}.list-group-item-danger{color:#78281f;background-color:#f8cdc8}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#78281f;background-color:#f5b8b1}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#78281f;border-color:#78281f}.list-group-item-light{color:#5a5e62;background-color:#e8eaed}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#5a5e62;background-color:#dadde2}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#5a5e62;border-color:#5a5e62}.list-group-item-dark{color:#191919;background-color:#c5c5c5}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#191919;background-color:#b8b8b8}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#191919;border-color:#191919}.close{float:right;font-size:1.40625rem;font-weight:700;line-height:1;color:#fff;text-shadow:none;opacity:.5}.close:hover{color:#fff;text-decoration:none}.close:not(:disabled):not(.disabled):hover,.close:not(:disabled):not(.disabled):focus{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:0.875rem;background-color:#444;background-clip:padding-box;border:1px solid rgba(0,0,0,0.1);-webkit-box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:0.25rem}.toast:not(:last-child){margin-bottom:0.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0.25rem 0.75rem;color:#888;background-color:#303030;background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,0.05)}.toast-body{padding:0.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:0.5rem;pointer-events:none}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform 0.3s ease-out;transition:-webkit-transform 0.3s ease-out;transition:transform 0.3s ease-out;transition:transform 0.3s ease-out, -webkit-transform 0.3s ease-out;-webkit-transform:translate(0, -50px);transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{-webkit-transition:none;transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-webkit-box;display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-header,.modal-dialog-scrollable .modal-footer{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#303030;background-clip:padding-box;border:1px solid #444;border-radius:0.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:0.5}.modal-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #444;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;padding:0.75rem;border-top:1px solid #444;border-bottom-right-radius:calc(0.3rem - 1px);border-bottom-left-radius:calc(0.3rem - 1px)}.modal-footer>*{margin:0.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width: 1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.8203125rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:0.9}.tooltip .arrow{position:absolute;display:block;width:0.8rem;height:0.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"]{padding:0.4rem 0}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow{bottom:0}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before{top:0;border-width:0.4rem 0.4rem 0;border-top-color:#000}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"]{padding:0 0.4rem}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow{left:0;width:0.4rem;height:0.8rem}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before{right:0;border-width:0.4rem 0.4rem 0.4rem 0;border-right-color:#000}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"]{padding:0.4rem 0}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow{top:0}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before{bottom:0;border-width:0 0.4rem 0.4rem;border-bottom-color:#000}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"]{padding:0 0.4rem}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow{right:0;width:0.4rem;height:0.8rem}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before{left:0;border-width:0.4rem 0 0.4rem 0.4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:0.25rem 0.5rem;color:#fff;text-align:center;background-color:#000;border-radius:0.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.8203125rem;word-wrap:break-word;background-color:#303030;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:0.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:0.5rem;margin:0 0.3rem}.popover .arrow::before,.popover .arrow::after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-top,.bs-popover-auto[x-placement^="top"]{margin-bottom:0.5rem}.bs-popover-top>.arrow,.bs-popover-auto[x-placement^="top"]>.arrow{bottom:calc(-0.5rem - 1px)}.bs-popover-top>.arrow::before,.bs-popover-auto[x-placement^="top"]>.arrow::before{bottom:0;border-width:0.5rem 0.5rem 0;border-top-color:rgba(0,0,0,0.25)}.bs-popover-top>.arrow::after,.bs-popover-auto[x-placement^="top"]>.arrow::after{bottom:1px;border-width:0.5rem 0.5rem 0;border-top-color:#303030}.bs-popover-right,.bs-popover-auto[x-placement^="right"]{margin-left:0.5rem}.bs-popover-right>.arrow,.bs-popover-auto[x-placement^="right"]>.arrow{left:calc(-0.5rem - 1px);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-right>.arrow::before,.bs-popover-auto[x-placement^="right"]>.arrow::before{left:0;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:rgba(0,0,0,0.25)}.bs-popover-right>.arrow::after,.bs-popover-auto[x-placement^="right"]>.arrow::after{left:1px;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:#303030}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"]{margin-top:0.5rem}.bs-popover-bottom>.arrow,.bs-popover-auto[x-placement^="bottom"]>.arrow{top:calc(-0.5rem - 1px)}.bs-popover-bottom>.arrow::before,.bs-popover-auto[x-placement^="bottom"]>.arrow::before{top:0;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:rgba(0,0,0,0.25)}.bs-popover-bottom>.arrow::after,.bs-popover-auto[x-placement^="bottom"]>.arrow::after{top:1px;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:#303030}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-0.5rem;content:"";border-bottom:1px solid #444}.bs-popover-left,.bs-popover-auto[x-placement^="left"]{margin-right:0.5rem}.bs-popover-left>.arrow,.bs-popover-auto[x-placement^="left"]>.arrow{right:calc(-0.5rem - 1px);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-left>.arrow::before,.bs-popover-auto[x-placement^="left"]>.arrow::before{right:0;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:rgba(0,0,0,0.25)}.bs-popover-left>.arrow::after,.bs-popover-auto[x-placement^="left"]>.arrow::after{right:1px;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:#303030}.popover-header{padding:0.5rem 0.75rem;margin-bottom:0;font-size:0.9375rem;background-color:#444;border-bottom:1px solid #373737;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:0.5rem 0.75rem;color:#fff}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transition:-webkit-transform 0.6s ease-in-out;transition:-webkit-transform 0.6s ease-in-out;transition:transform 0.6s ease-in-out;transition:transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out}@media (prefers-reduced-motion: reduce){.carousel-item{-webkit-transition:none;transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-left),.active.carousel-item-right{-webkit-transform:translateX(100%);transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-right),.active.carousel-item-left{-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;-webkit-transition-property:opacity;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;-webkit-transition:opacity 0s 0.6s;transition:opacity 0s 0.6s}@media (prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{-webkit-transition:none;transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:0.5;-webkit-transition:opacity 0.15s ease;transition:opacity 0.15s ease}@media (prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{-webkit-transition:none;transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:0.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50% / 100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;-webkit-transition:opacity 0.6s ease;transition:opacity 0.6s ease}@media (prefers-reduced-motion: reduce){.carousel-indicators li{-webkit-transition:none;transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:0.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:0.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-primary{background-color:#375a7f !important}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus{background-color:#28415b !important}.bg-secondary{background-color:#444 !important}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus{background-color:#2b2a2a !important}.bg-success{background-color:#00bc8c !important}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus{background-color:#008966 !important}.bg-info{background-color:#3498DB !important}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus{background-color:#217dbb !important}.bg-warning{background-color:#F39C12 !important}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus{background-color:#c87f0a !important}.bg-danger{background-color:#E74C3C !important}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus{background-color:#d62c1a !important}.bg-light{background-color:#adb5bd !important}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus{background-color:#919ca6 !important}.bg-dark{background-color:#303030 !important}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus{background-color:#171616 !important}.bg-white{background-color:#fff !important}.bg-transparent{background-color:transparent !important}.border{border:1px solid #dee2e6 !important}.border-top{border-top:1px solid #dee2e6 !important}.border-right{border-right:1px solid #dee2e6 !important}.border-bottom{border-bottom:1px solid #dee2e6 !important}.border-left{border-left:1px solid #dee2e6 !important}.border-0{border:0 !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-primary{border-color:#375a7f !important}.border-secondary{border-color:#444 !important}.border-success{border-color:#00bc8c !important}.border-info{border-color:#3498DB !important}.border-warning{border-color:#F39C12 !important}.border-danger{border-color:#E74C3C !important}.border-light{border-color:#adb5bd !important}.border-dark{border-color:#303030 !important}.border-white{border-color:#fff !important}.rounded-sm{border-radius:0.2rem !important}.rounded{border-radius:0.25rem !important}.rounded-top{border-top-left-radius:0.25rem !important;border-top-right-radius:0.25rem !important}.rounded-right{border-top-right-radius:0.25rem !important;border-bottom-right-radius:0.25rem !important}.rounded-bottom{border-bottom-right-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-left{border-top-left-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-lg{border-radius:0.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-sm-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-md-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-lg-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-xl-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-print-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.8571428571%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-sm-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-sm-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-sm-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-sm-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-sm-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-sm-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-sm-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-sm-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-sm-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-sm-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-sm-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-sm-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-sm-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-sm-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-sm-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-sm-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-sm-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-sm-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-sm-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-sm-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-sm-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-sm-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-sm-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-sm-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-sm-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-sm-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-sm-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-sm-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-sm-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-sm-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-sm-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-sm-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-md-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-md-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-md-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-md-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-md-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-md-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-md-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-md-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-md-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-md-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-md-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-md-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-md-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-md-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-md-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-md-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-md-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-md-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-md-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-md-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-md-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-md-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-md-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-md-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-md-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-md-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-md-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-md-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-md-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-md-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-md-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-md-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-lg-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-lg-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-lg-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-lg-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-lg-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-lg-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-lg-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-lg-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-lg-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-lg-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-lg-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-lg-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-lg-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-lg-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-lg-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-lg-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-lg-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-lg-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-lg-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-lg-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-lg-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-lg-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-lg-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-lg-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-lg-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-lg-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-lg-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-lg-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-lg-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-lg-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-lg-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-lg-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-xl-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-xl-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-xl-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-xl-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-xl-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-xl-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-xl-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-xl-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-xl-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-xl-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-xl-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-xl-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-xl-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-xl-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-xl-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-xl-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-xl-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-xl-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-xl-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-xl-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-xl-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-xl-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-xl-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-xl-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-xl-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-xl-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-xl-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-xl-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-xl-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-xl-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-xl-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-xl-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.user-select-all{-webkit-user-select:all !important;-moz-user-select:all !important;-ms-user-select:all !important;user-select:all !important}.user-select-auto{-webkit-user-select:auto !important;-moz-user-select:auto !important;-ms-user-select:auto !important;user-select:auto !important}.user-select-none{-webkit-user-select:none !important;-moz-user-select:none !important;-ms-user-select:none !important;user-select:none !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:-webkit-sticky !important;position:sticky !important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position: -webkit-sticky) or (position: sticky){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{-webkit-box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important;box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important}.shadow{-webkit-box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important;box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important}.shadow-lg{-webkit-box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important;box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important}.shadow-none{-webkit-box-shadow:none !important;box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.min-vw-100{min-width:100vw !important}.min-vh-100{min-height:100vh !important}.vw-100{width:100vw !important}.vh-100{height:100vh !important}.m-0{margin:0 !important}.mt-0,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1{margin:0.25rem !important}.mt-1,.my-1{margin-top:0.25rem !important}.mr-1,.mx-1{margin-right:0.25rem !important}.mb-1,.my-1{margin-bottom:0.25rem !important}.ml-1,.mx-1{margin-left:0.25rem !important}.m-2{margin:0.5rem !important}.mt-2,.my-2{margin-top:0.5rem !important}.mr-2,.mx-2{margin-right:0.5rem !important}.mb-2,.my-2{margin-bottom:0.5rem !important}.ml-2,.mx-2{margin-left:0.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.mx-3{margin-right:1rem !important}.mb-3,.my-3{margin-bottom:1rem !important}.ml-3,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0{padding:0 !important}.pt-0,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.py-0{padding-bottom:0 !important}.pl-0,.px-0{padding-left:0 !important}.p-1{padding:0.25rem !important}.pt-1,.py-1{padding-top:0.25rem !important}.pr-1,.px-1{padding-right:0.25rem !important}.pb-1,.py-1{padding-bottom:0.25rem !important}.pl-1,.px-1{padding-left:0.25rem !important}.p-2{padding:0.5rem !important}.pt-2,.py-2{padding-top:0.5rem !important}.pr-2,.px-2{padding-right:0.5rem !important}.pb-2,.py-2{padding-bottom:0.5rem !important}.pl-2,.px-2{padding-left:0.5rem !important}.p-3{padding:1rem !important}.pt-3,.py-3{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3{padding-bottom:1rem !important}.pl-3,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-n1{margin:-0.25rem !important}.mt-n1,.my-n1{margin-top:-0.25rem !important}.mr-n1,.mx-n1{margin-right:-0.25rem !important}.mb-n1,.my-n1{margin-bottom:-0.25rem !important}.ml-n1,.mx-n1{margin-left:-0.25rem !important}.m-n2{margin:-0.5rem !important}.mt-n2,.my-n2{margin-top:-0.5rem !important}.mr-n2,.mx-n2{margin-right:-0.5rem !important}.mb-n2,.my-n2{margin-bottom:-0.5rem !important}.ml-n2,.mx-n2{margin-left:-0.5rem !important}.m-n3{margin:-1rem !important}.mt-n3,.my-n3{margin-top:-1rem !important}.mr-n3,.mx-n3{margin-right:-1rem !important}.mb-n3,.my-n3{margin-bottom:-1rem !important}.ml-n3,.mx-n3{margin-left:-1rem !important}.m-n4{margin:-1.5rem !important}.mt-n4,.my-n4{margin-top:-1.5rem !important}.mr-n4,.mx-n4{margin-right:-1.5rem !important}.mb-n4,.my-n4{margin-bottom:-1.5rem !important}.ml-n4,.mx-n4{margin-left:-1.5rem !important}.m-n5{margin:-3rem !important}.mt-n5,.my-n5{margin-top:-3rem !important}.mr-n5,.mx-n5{margin-right:-3rem !important}.mb-n5,.my-n5{margin-bottom:-3rem !important}.ml-n5,.mx-n5{margin-left:-3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:0.25rem !important}.mt-sm-1,.my-sm-1{margin-top:0.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:0.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:0.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:0.25rem !important}.m-sm-2{margin:0.5rem !important}.mt-sm-2,.my-sm-2{margin-top:0.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:0.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:0.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:0.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:0.25rem !important}.pt-sm-1,.py-sm-1{padding-top:0.25rem !important}.pr-sm-1,.px-sm-1{padding-right:0.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:0.25rem !important}.pl-sm-1,.px-sm-1{padding-left:0.25rem !important}.p-sm-2{padding:0.5rem !important}.pt-sm-2,.py-sm-2{padding-top:0.5rem !important}.pr-sm-2,.px-sm-2{padding-right:0.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:0.5rem !important}.pl-sm-2,.px-sm-2{padding-left:0.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-n1{margin:-0.25rem !important}.mt-sm-n1,.my-sm-n1{margin-top:-0.25rem !important}.mr-sm-n1,.mx-sm-n1{margin-right:-0.25rem !important}.mb-sm-n1,.my-sm-n1{margin-bottom:-0.25rem !important}.ml-sm-n1,.mx-sm-n1{margin-left:-0.25rem !important}.m-sm-n2{margin:-0.5rem !important}.mt-sm-n2,.my-sm-n2{margin-top:-0.5rem !important}.mr-sm-n2,.mx-sm-n2{margin-right:-0.5rem !important}.mb-sm-n2,.my-sm-n2{margin-bottom:-0.5rem !important}.ml-sm-n2,.mx-sm-n2{margin-left:-0.5rem !important}.m-sm-n3{margin:-1rem !important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem !important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem !important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem !important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem !important}.m-sm-n4{margin:-1.5rem !important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem !important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem !important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem !important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem !important}.m-sm-n5{margin:-3rem !important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem !important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem !important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem !important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:0.25rem !important}.mt-md-1,.my-md-1{margin-top:0.25rem !important}.mr-md-1,.mx-md-1{margin-right:0.25rem !important}.mb-md-1,.my-md-1{margin-bottom:0.25rem !important}.ml-md-1,.mx-md-1{margin-left:0.25rem !important}.m-md-2{margin:0.5rem !important}.mt-md-2,.my-md-2{margin-top:0.5rem !important}.mr-md-2,.mx-md-2{margin-right:0.5rem !important}.mb-md-2,.my-md-2{margin-bottom:0.5rem !important}.ml-md-2,.mx-md-2{margin-left:0.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:0.25rem !important}.pt-md-1,.py-md-1{padding-top:0.25rem !important}.pr-md-1,.px-md-1{padding-right:0.25rem !important}.pb-md-1,.py-md-1{padding-bottom:0.25rem !important}.pl-md-1,.px-md-1{padding-left:0.25rem !important}.p-md-2{padding:0.5rem !important}.pt-md-2,.py-md-2{padding-top:0.5rem !important}.pr-md-2,.px-md-2{padding-right:0.5rem !important}.pb-md-2,.py-md-2{padding-bottom:0.5rem !important}.pl-md-2,.px-md-2{padding-left:0.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-n1{margin:-0.25rem !important}.mt-md-n1,.my-md-n1{margin-top:-0.25rem !important}.mr-md-n1,.mx-md-n1{margin-right:-0.25rem !important}.mb-md-n1,.my-md-n1{margin-bottom:-0.25rem !important}.ml-md-n1,.mx-md-n1{margin-left:-0.25rem !important}.m-md-n2{margin:-0.5rem !important}.mt-md-n2,.my-md-n2{margin-top:-0.5rem !important}.mr-md-n2,.mx-md-n2{margin-right:-0.5rem !important}.mb-md-n2,.my-md-n2{margin-bottom:-0.5rem !important}.ml-md-n2,.mx-md-n2{margin-left:-0.5rem !important}.m-md-n3{margin:-1rem !important}.mt-md-n3,.my-md-n3{margin-top:-1rem !important}.mr-md-n3,.mx-md-n3{margin-right:-1rem !important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem !important}.ml-md-n3,.mx-md-n3{margin-left:-1rem !important}.m-md-n4{margin:-1.5rem !important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem !important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem !important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem !important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem !important}.m-md-n5{margin:-3rem !important}.mt-md-n5,.my-md-n5{margin-top:-3rem !important}.mr-md-n5,.mx-md-n5{margin-right:-3rem !important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem !important}.ml-md-n5,.mx-md-n5{margin-left:-3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:0.25rem !important}.mt-lg-1,.my-lg-1{margin-top:0.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:0.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:0.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:0.25rem !important}.m-lg-2{margin:0.5rem !important}.mt-lg-2,.my-lg-2{margin-top:0.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:0.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:0.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:0.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:0.25rem !important}.pt-lg-1,.py-lg-1{padding-top:0.25rem !important}.pr-lg-1,.px-lg-1{padding-right:0.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:0.25rem !important}.pl-lg-1,.px-lg-1{padding-left:0.25rem !important}.p-lg-2{padding:0.5rem !important}.pt-lg-2,.py-lg-2{padding-top:0.5rem !important}.pr-lg-2,.px-lg-2{padding-right:0.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:0.5rem !important}.pl-lg-2,.px-lg-2{padding-left:0.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-n1{margin:-0.25rem !important}.mt-lg-n1,.my-lg-n1{margin-top:-0.25rem !important}.mr-lg-n1,.mx-lg-n1{margin-right:-0.25rem !important}.mb-lg-n1,.my-lg-n1{margin-bottom:-0.25rem !important}.ml-lg-n1,.mx-lg-n1{margin-left:-0.25rem !important}.m-lg-n2{margin:-0.5rem !important}.mt-lg-n2,.my-lg-n2{margin-top:-0.5rem !important}.mr-lg-n2,.mx-lg-n2{margin-right:-0.5rem !important}.mb-lg-n2,.my-lg-n2{margin-bottom:-0.5rem !important}.ml-lg-n2,.mx-lg-n2{margin-left:-0.5rem !important}.m-lg-n3{margin:-1rem !important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem !important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem !important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem !important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem !important}.m-lg-n4{margin:-1.5rem !important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem !important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem !important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem !important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem !important}.m-lg-n5{margin:-3rem !important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem !important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem !important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem !important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:0.25rem !important}.mt-xl-1,.my-xl-1{margin-top:0.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:0.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:0.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:0.25rem !important}.m-xl-2{margin:0.5rem !important}.mt-xl-2,.my-xl-2{margin-top:0.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:0.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:0.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:0.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:0.25rem !important}.pt-xl-1,.py-xl-1{padding-top:0.25rem !important}.pr-xl-1,.px-xl-1{padding-right:0.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:0.25rem !important}.pl-xl-1,.px-xl-1{padding-left:0.25rem !important}.p-xl-2{padding:0.5rem !important}.pt-xl-2,.py-xl-2{padding-top:0.5rem !important}.pr-xl-2,.px-xl-2{padding-right:0.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:0.5rem !important}.pl-xl-2,.px-xl-2{padding-left:0.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-n1{margin:-0.25rem !important}.mt-xl-n1,.my-xl-n1{margin-top:-0.25rem !important}.mr-xl-n1,.mx-xl-n1{margin-right:-0.25rem !important}.mb-xl-n1,.my-xl-n1{margin-bottom:-0.25rem !important}.ml-xl-n1,.mx-xl-n1{margin-left:-0.25rem !important}.m-xl-n2{margin:-0.5rem !important}.mt-xl-n2,.my-xl-n2{margin-top:-0.5rem !important}.mr-xl-n2,.mx-xl-n2{margin-right:-0.5rem !important}.mb-xl-n2,.my-xl-n2{margin-bottom:-0.5rem !important}.ml-xl-n2,.mx-xl-n2{margin-left:-0.5rem !important}.m-xl-n3{margin:-1rem !important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem !important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem !important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem !important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem !important}.m-xl-n4{margin:-1.5rem !important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem !important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem !important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem !important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem !important}.m-xl-n5{margin:-3rem !important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem !important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem !important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem !important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important}.text-justify{text-align:justify !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right{text-align:right !important}.text-center{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-primary{color:#375a7f !important}a.text-primary:hover,a.text-primary:focus{color:#20344a !important}.text-secondary{color:#444 !important}a.text-secondary:hover,a.text-secondary:focus{color:#1e1e1e !important}.text-success{color:#00bc8c !important}a.text-success:hover,a.text-success:focus{color:#007053 !important}.text-info{color:#3498DB !important}a.text-info:hover,a.text-info:focus{color:#1d6fa5 !important}.text-warning{color:#F39C12 !important}a.text-warning:hover,a.text-warning:focus{color:#b06f09 !important}.text-danger{color:#E74C3C !important}a.text-danger:hover,a.text-danger:focus{color:#bf2718 !important}.text-light{color:#adb5bd !important}a.text-light:hover,a.text-light:focus{color:#838f9b !important}.text-dark{color:#303030 !important}a.text-dark:hover,a.text-dark:focus{color:#0a0a0a !important}.text-body{color:#fff !important}.text-muted{color:#888 !important}.text-black-50{color:rgba(0,0,0,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none !important}.text-break{word-wrap:break-word !important}.text-reset{color:inherit !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media print{*,*::before,*::after{text-shadow:none !important;-webkit-box-shadow:none !important;box-shadow:none !important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap !important}pre,blockquote{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px !important}.container{min-width:992px !important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #dee2e6 !important}.table-dark{color:inherit}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#444}.table .thead-dark th{color:inherit;border-color:#444}}.blockquote-footer{color:#888}.table-primary,.table-primary>th,.table-primary>td{background-color:#375a7f}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#444}.table-light,.table-light>th,.table-light>td{background-color:#adb5bd}.table-dark,.table-dark>th,.table-dark>td{background-color:#303030}.table-success,.table-success>th,.table-success>td{background-color:#00bc8c}.table-info,.table-info>th,.table-info>td{background-color:#3498DB}.table-danger,.table-danger>th,.table-danger>td{background-color:#E74C3C}.table-warning,.table-warning>th,.table-warning>td{background-color:#F39C12}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-primary:hover,.table-hover .table-primary:hover>th,.table-hover .table-primary:hover>td{background-color:#2f4d6d}.table-hover .table-secondary:hover,.table-hover .table-secondary:hover>th,.table-hover .table-secondary:hover>td{background-color:#373737}.table-hover .table-light:hover,.table-hover .table-light:hover>th,.table-hover .table-light:hover>td{background-color:#9fa8b2}.table-hover .table-dark:hover,.table-hover .table-dark:hover>th,.table-hover .table-dark:hover>td{background-color:#232323}.table-hover .table-success:hover,.table-hover .table-success:hover>th,.table-hover .table-success:hover>td{background-color:#00a379}.table-hover .table-info:hover,.table-hover .table-info:hover>th,.table-hover .table-info:hover>td{background-color:#258cd1}.table-hover .table-danger:hover,.table-hover .table-danger:hover>th,.table-hover .table-danger:hover>td{background-color:#e43725}.table-hover .table-warning:hover,.table-hover .table-warning:hover>th,.table-hover .table-warning:hover>td{background-color:#e08e0b}.table-hover .table-active:hover,.table-hover .table-active:hover>th,.table-hover .table-active:hover>td{background-color:rgba(0,0,0,0.075)}.input-group-addon{color:#fff}.nav-tabs .nav-link,.nav-tabs .nav-link.active,.nav-tabs .nav-link.active:focus,.nav-tabs .nav-link.active:hover,.nav-tabs .nav-item.open .nav-link,.nav-tabs .nav-item.open .nav-link:focus,.nav-tabs .nav-item.open .nav-link:hover,.nav-pills .nav-link,.nav-pills .nav-link.active,.nav-pills .nav-link.active:focus,.nav-pills .nav-link.active:hover,.nav-pills .nav-item.open .nav-link,.nav-pills .nav-item.open .nav-link:focus,.nav-pills .nav-item.open .nav-link:hover{color:#fff}.breadcrumb a{color:#fff}.pagination a:hover{text-decoration:none}.close{opacity:0.4}.close:hover,.close:focus{opacity:1}.alert{border:none;color:#fff}.alert a,.alert .alert-link{color:#fff;text-decoration:underline}.alert-primary{background-color:#375a7f}.alert-secondary{background-color:#444}.alert-success{background-color:#00bc8c}.alert-info{background-color:#3498DB}.alert-warning{background-color:#F39C12}.alert-danger{background-color:#E74C3C}.alert-light{background-color:#adb5bd}.alert-dark{background-color:#303030}.list-group-item-action{color:#fff}.list-group-item-action:hover,.list-group-item-action:focus{background-color:#444;color:#fff}.list-group-item-action .list-group-item-heading{color:#fff} diff --git a/htdocs/css/features.css b/htdocs/css/features.css index d52cae9..be41fef 100644 --- a/htdocs/css/features.css +++ b/htdocs/css/features.css @@ -5,7 +5,3 @@ h1 { text-align: center; margin: 50px 0; } - -.table { - color: inherit; -} \ No newline at end of file diff --git a/htdocs/css/login.css b/htdocs/css/login.css index c50341e..5e51dc9 100644 --- a/htdocs/css/login.css +++ b/htdocs/css/login.css @@ -1,10 +1,6 @@ @import url("openwebrx-header.css"); @import url("openwebrx-globals.css"); -body { - background-color: #2e2e2e; -} - .login { position: absolute; left: 50%; @@ -13,7 +9,6 @@ body { width: 500px; - background-color: #ddd; padding: 20px; border-radius: 10px; border: 1px solid #575757; @@ -25,8 +20,5 @@ body { } .btn-login { - color: #FFF; - background-color: #2e2e2e; - border-color: #2e2e2e; height: 50px; } \ No newline at end of file diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css index 09c1fed..412e104 100644 --- a/htdocs/css/openwebrx-header.css +++ b/htdocs/css/openwebrx-header.css @@ -43,6 +43,15 @@ right: 0; } +#webrx-tob-container, #webrx-top-container * { + line-height: initial; + box-sizing: initial; +} + +#webrx-top-container img { + vertical-align: initial; +} + #webrx-top-logo { padding: 12px; diff --git a/htdocs/features.html b/htdocs/features.html index 2a97cd1..90d156e 100644 --- a/htdocs/features.html +++ b/htdocs/features.html @@ -1,7 +1,7 @@ OpenWebRX Feature report - + diff --git a/htdocs/generalsettings.html b/htdocs/generalsettings.html index ae0656c..3367ed8 100644 --- a/htdocs/generalsettings.html +++ b/htdocs/generalsettings.html @@ -3,7 +3,7 @@ OpenWebRX Settings - + diff --git a/htdocs/login.html b/htdocs/login.html index 9cbeacc..4f4c554 100644 --- a/htdocs/login.html +++ b/htdocs/login.html @@ -3,7 +3,7 @@ OpenWebRX Login - + @@ -21,7 +21,7 @@
- +
\ No newline at end of file diff --git a/htdocs/sdrsettings.html b/htdocs/sdrsettings.html index 08664fe..89e6a8f 100644 --- a/htdocs/sdrsettings.html +++ b/htdocs/sdrsettings.html @@ -3,7 +3,7 @@ OpenWebRX Settings - + diff --git a/htdocs/settings.html b/htdocs/settings.html index 6e6bd89..39cecf9 100644 --- a/htdocs/settings.html +++ b/htdocs/settings.html @@ -3,7 +3,7 @@ OpenWebRX Settings - + From 048aab682f2343529fa8c851cb7cc7f48cefaaa0 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 May 2020 22:56:49 +0200 Subject: [PATCH 328/475] include changed wsjt keys in config migration --- owrx/config.py | 8 ++++++++ owrx/property/__init__.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/owrx/config.py b/owrx/config.py index 4aa1c8e..bdecd04 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -26,6 +26,11 @@ class ConfigMigrator(ABC): def migrate(self, config): pass + def renameKey(self, config, old, new): + if old in config and not new in config: + config[new] = config[old] + del config[old] + class ConfigMigratorVersion1(ConfigMigrator): def migrate(self, config): @@ -37,6 +42,9 @@ class ConfigMigratorVersion1(ConfigMigrator): levels = config["waterfall_auto_level_margin"] config["waterfall_auto_level_margin"] = {"min": levels[0], "max": levels[1]} + self.renameKey(config, "wsjt_queue_workers", "decoding_queue_workers") + self.renameKey(config, "wsjt_queue_length", "decoding_queue_length") + config["version"] = 2 return config diff --git a/owrx/property/__init__.py b/owrx/property/__init__.py index 853cca3..f3560fa 100644 --- a/owrx/property/__init__.py +++ b/owrx/property/__init__.py @@ -40,6 +40,10 @@ class PropertyManager(ABC): def __dict__(self): pass + @abstractmethod + def __delitem__(self, key): + pass + @abstractmethod def keys(self): pass @@ -98,6 +102,9 @@ class PropertyLayer(PropertyManager): def __dict__(self): return {k: v for k, v in self.properties.items()} + def __delitem__(self, key): + return self.properties.__delitem__(key) + def keys(self): return self.properties.keys() @@ -132,6 +139,11 @@ class PropertyFilter(PropertyManager): def __dict__(self): return {k: v for k, v in self.pm.__dict__().items() if k in self.props} + def __delitem__(self, key): + if key not in self.props: + raise KeyError(key) + return self.pm.__delitem__(key) + def keys(self): return [k for k in self.pm.keys() if k in self.props] @@ -226,5 +238,9 @@ class PropertyStack(PropertyManager): def __dict__(self): return {k: self.__getitem__(k) for k in self.keys()} + def __delitem__(self, key): + for layer in self.layers: + layer["props"].__delitem__(key) + def keys(self): return set([key for l in self.layers for key in l["props"].keys()]) From 2b4799591f695be5d4548da4d69bb57f719b2fc1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 14 May 2020 22:57:09 +0200 Subject: [PATCH 329/475] initialize logging early since there may be messages happening in imports --- owrx/__main__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/owrx/__main__.py b/owrx/__main__.py index 8e082e9..84bca23 100644 --- a/owrx/__main__.py +++ b/owrx/__main__.py @@ -1,3 +1,8 @@ +import logging + +logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + from http.server import HTTPServer from owrx.http import RequestHandler from owrx.config import Config @@ -10,11 +15,6 @@ from owrx.websocket import WebSocketConnection from owrx.pskreporter import PskReporter from owrx.version import openwebrx_version -import logging - -logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") -logger = logging.getLogger(__name__) - class ThreadedHttpServer(ThreadingMixIn, HTTPServer): pass From 0a60b505b8fa6ad54f632b33d62d37a76b0f799f Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 16 May 2020 19:03:48 +0200 Subject: [PATCH 330/475] update dependencies, refs #38 --- docker/scripts/install-connectors.sh | 2 +- docker/scripts/install-dependencies.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/scripts/install-connectors.sh b/docker/scripts/install-connectors.sh index cc2572d..b848e7a 100755 --- a/docker/scripts/install-connectors.sh +++ b/docker/scripts/install-connectors.sh @@ -24,6 +24,6 @@ apk add --no-cache --virtual .build-deps $BUILD_PACKAGES git clone https://github.com/jketterl/owrx_connector.git -cmakebuild owrx_connector 9d72cf1382ed90735632a6d0ef6f820a4758f733 +cmakebuild owrx_connector 3ffbdcccbe39a5675fd539d959e64885e521eae6 apk del .build-deps diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 1f195d7..f416802 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -35,7 +35,7 @@ cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d git clone https://github.com/jketterl/csdr.git cd csdr -git checkout fe0b042d9cdc2605a817ca7fdd3a23c48bf07563 +git checkout a1424eac3163a8756219608fe509b036bf90b129 make make install cd .. From d22ab2377163d205e511abf641f5316ea50c40a2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 16 May 2020 21:29:16 +0200 Subject: [PATCH 331/475] set package build flag to disable optimizations --- docker/scripts/install-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index f416802..991d08b 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -36,7 +36,7 @@ cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d git clone https://github.com/jketterl/csdr.git cd csdr git checkout a1424eac3163a8756219608fe509b036bf90b129 -make +CSDR_PACKAGE_BUILD=1 make make install cd .. rm -rf csdr From ddbc844954cce4cbb98a4be6e446564fc2c7e6fa Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 17 May 2020 01:38:08 +0200 Subject: [PATCH 332/475] update csdr --- docker/scripts/install-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 991d08b..90b053d 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -35,7 +35,7 @@ cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d git clone https://github.com/jketterl/csdr.git cd csdr -git checkout a1424eac3163a8756219608fe509b036bf90b129 +git checkout 29efbfb961a435fc5d81b0da269deb4a116b3f1c CSDR_PACKAGE_BUILD=1 make make install cd .. From 98cb1a8389ad68085fa09173e6807db85c79e0ef Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 17 May 2020 11:19:22 +0200 Subject: [PATCH 333/475] use the new version without FMV --- docker/scripts/install-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 90b053d..cfd87df 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -35,7 +35,7 @@ cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d git clone https://github.com/jketterl/csdr.git cd csdr -git checkout 29efbfb961a435fc5d81b0da269deb4a116b3f1c +git checkout 973de7b471d043a3434f9eb8b56e38fa281d4623 CSDR_PACKAGE_BUILD=1 make make install cd .. From a03176223acc59cd093ab97e48886aee4270bf37 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 17 May 2020 18:44:26 +0200 Subject: [PATCH 334/475] add a bit more dynamic content --- htdocs/settings.js | 133 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 113 insertions(+), 20 deletions(-) diff --git a/htdocs/settings.js b/htdocs/settings.js index 55abf45..2d21188 100644 --- a/htdocs/settings.js +++ b/htdocs/settings.js @@ -1,13 +1,14 @@ -function Input(name, value) { +function Input(name, value, label) { this.name = name; this.value = value; + this.label = label; }; -Input.prototype.bootstrapify = function(input, label) { +Input.prototype.bootstrapify = function(input) { input.addClass('form-control').addClass('form-control-sm'); return [ '
', - '', + '', '
', input[0].outerHTML, '
', @@ -55,34 +56,104 @@ SchedulerInput.prototype.render = function() { return $('

Scheduler

'); }; -Input.mappings = { - "name": TextInput, - "type": TextInput, - "ppm": NumberInput, - "profiles": ProfileInput, - "scheduler": SchedulerInput -}; - -function SdrDevice(el) { +function SdrDevice(el, data) { this.el = el; - this.data = JSON.parse(decodeURIComponent(el.data('config'))); + this.data = data; this.inputs = {}; this.render(); var self = this; el.on('click', '.fieldselector .btn', function() { var key = el.find('.fieldselector select').val(); - self.data[key] = false; + self.data[key] = self.getInitialValue(key); self.render(); }); }; +SdrDevice.create = function(el) { + var data = JSON.parse(decodeURIComponent(el.data('config'))); + var type = data.type; + var constructor = SdrDevice.types[type] || SdrDevice; + return new constructor(el, data); +}; + +SdrDevice.prototype.getData = function() { + return $.extend(new Object(), this.getDefaults(), this.data); +}; + +SdrDevice.prototype.getDefaults = function() { + var defaults = {} + $.each(this.getMappings(), function(k, v) { + if (!v.includeInDefault) return; + defaults[k] = 'initialValue' in v ? v['initialValue'] : false; + }); + return defaults; +}; + +SdrDevice.prototype.getMappings = function() { + return { + "name": { + constructor: TextInput, + label: "Name", + initialValue: "", + includeInDefault: true + }, + "type": { + constructor: TextInput, + label: "Type", + initialValue: '', + includeInDefault: true + }, + "ppm": { + constructor: NumberInput, + label: "PPM", + initialValue: 0 + }, + "profiles": { + constructor: ProfileInput, + label: "Profiles", + initialValue: [], + includeInDefault: true, + }, + "scheduler": { + constructor: SchedulerInput, + label: "Scheduler", + initialValue: {} + }, + "rf_gain": { + constructor: TextInput, + label: "Gain", + initialValue: 0 + } + }; +}; + +SdrDevice.prototype.getMapping = function(key) { + var mappings = this.getMappings(); + return mappings[key]; +}; + +SdrDevice.prototype.getInputClass = function(key) { + var mapping = this.getMapping(key); + return mapping && mapping.constructor || TextInput; +}; + +SdrDevice.prototype.getLabel = function(key) { + var mapping = this.getMapping(key); + return mapping && mapping.label || key; +}; + +SdrDevice.prototype.getInitialValue = function(key) { + var mapping = this.getMapping(key); + return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false; +}; + SdrDevice.prototype.render = function() { var self = this; self.el.empty(); - $.each(this.data, function(key, value) { - var inputClass = Input.mappings[key] || TextInput; - var input = new inputClass(key, value); + $.each(this.getData(), function(key, value) { + var inputClass = self.getInputClass(key); + var input = new inputClass(key, value, self.getLabel(key)); self.inputs[key] = input; self.el.append(input.render()); }); @@ -95,10 +166,10 @@ SdrDevice.prototype.renderFieldSelector = function() { '

Add new configuration options

' + '
' + '
' + '
' + @@ -108,11 +179,33 @@ SdrDevice.prototype.renderFieldSelector = function() { '
'; }; +RtlSdrDevice = function(el, data) { + SdrDevice.apply(this, arguments); +}; + +RtlSdrDevice.prototype = Object.create(SdrDevice.prototype); +RtlSdrDevice.prototype.constructor = RtlSdrDevice; + +RtlSdrDevice.prototype.getMappings = function() { + var mappings = SdrDevice.prototype.getMappings.apply(this, arguments); + return $.extend(new Object(), mappings, { + "device": { + constructor: TextInput, + label: "Serial Number", + initialValue: "" + } + }); +}; + +SdrDevice.types = { + 'rtl_sdr': RtlSdrDevice +}; + $.fn.sdrdevice = function() { return this.map(function(){ var el = $(this); if (!el.data('sdrdevice')) { - el.data('sdrdevice', new SdrDevice(el)); + el.data('sdrdevice', SdrDevice.create(el)); } return el.data('sdrdevice'); }); From b624bef345210ae898a3ebf38b1aebea985f59d1 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 17 May 2020 18:45:01 +0200 Subject: [PATCH 335/475] add broadcast bands --- bands.json | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/bands.json b/bands.json index 1629d4a..608c40f 100644 --- a/bands.json +++ b/bands.json @@ -199,5 +199,75 @@ "name": "3cm", "lower_bound": 10000000000, "upper_bound": 10500000000 + }, + { + "name": "120m Broadcast", + "lower_bound": 2300000, + "upper_bound": 2495000 + }, + { + "name": "90m Broadcast", + "lower_bound": 3200000, + "upper_bound": 3400000 + }, + { + "name": "75m Broadcast", + "lower_bound": 3900000, + "upper_bound": 4000000 + }, + { + "name": "60m Broadcast", + "lower_bound": 4750000, + "upper_bound": 4995000 + }, + { + "name": "49m Broadcast", + "lower_bound": 5900000, + "upper_bound": 6200000 + }, + { + "name": "41m Broadcast", + "lower_bound": 7200000, + "upper_bound": 7450000 + }, + { + "name": "31m Broadcast", + "lower_bound": 9400000, + "upper_bound": 9900000 + }, + { + "name": "25m Broadcast", + "lower_bound": 11600000, + "upper_bound": 12100000 + }, + { + "name": "22m Broadcast", + "lower_bound": 13570000, + "upper_bound": 13870000 + }, + { + "name": "19m Broadcast", + "lower_bound": 15100000, + "upper_bound": 15830000 + }, + { + "name": "16m Broadcast", + "lower_bound": 17480000, + "upper_bound": 17900000 + }, + { + "name": "15m Broadcast", + "lower_bound": 18900000, + "upper_bound": 19020000 + }, + { + "name": "13m Broadcast", + "lower_bound": 21450000, + "upper_bound": 21850000 + }, + { + "name": "11m Broadcast", + "lower_bound": 25670000, + "upper_bound": 26100000 } ] \ No newline at end of file From 9dd7a7e653f1533a928d6cd3a26a8cde7a539231 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 17 May 2020 18:51:36 +0200 Subject: [PATCH 336/475] remove the remnants of the templating configuration --- config_webrx.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/config_webrx.py b/config_webrx.py index fdde08e..15b96e4 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -49,11 +49,13 @@ receiver_asl = 200 receiver_admin = "example@example.com" receiver_gps = {"lat": 47.000000, "lon": 19.000000} photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory" +# photo_desc allows you to put pretty much any HTML you like into the receiver description. +# The lines below should give you some examples of what's possible. photo_desc = """ You can add your own background photo and receiver information.
-Receiver is operated by: %[RX_ADMIN]
-Device: %[RX_DEVICE]
-Antenna: %[RX_ANT]
+Receiver is operated by: Receiver Operator
+Device: Receiver Device
+Antenna: Receiver Antenna
Website: http://localhost """ From 63475dda78d68088d86ccdc21c6025ab759eb635 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 17 May 2020 20:25:49 +0200 Subject: [PATCH 337/475] implement field sorting --- htdocs/settings.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/htdocs/settings.js b/htdocs/settings.js index 2d21188..7936176 100644 --- a/htdocs/settings.js +++ b/htdocs/settings.js @@ -114,11 +114,13 @@ SdrDevice.prototype.getMappings = function() { label: "Profiles", initialValue: [], includeInDefault: true, + position: 100 }, "scheduler": { constructor: SchedulerInput, label: "Scheduler", - initialValue: {} + initialValue: {}, + position: 101 }, "rf_gain": { constructor: TextInput, @@ -148,10 +150,19 @@ SdrDevice.prototype.getInitialValue = function(key) { return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false; }; +SdrDevice.prototype.getPosition = function(key) { + var mapping = this.getMapping(key); + return mapping && mapping.position || 10; +}; + SdrDevice.prototype.render = function() { var self = this; self.el.empty(); - $.each(this.getData(), function(key, value) { + var data = this.getData(); + Object.keys(data).sort(function(a, b){ + return self.getPosition(a) - self.getPosition(b); + }).forEach(function(key){ + var value = data[key]; var inputClass = self.getInputClass(key); var input = new inputClass(key, value, self.getLabel(key)); self.inputs[key] = input; From 48b177defad424af2d307f956936221863aa9aa5 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 17 May 2020 21:21:37 +0200 Subject: [PATCH 338/475] provision for a custom gain control --- htdocs/settings.js | 106 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 89 insertions(+), 17 deletions(-) diff --git a/htdocs/settings.js b/htdocs/settings.js index 7936176..278e1c0 100644 --- a/htdocs/settings.js +++ b/htdocs/settings.js @@ -1,7 +1,8 @@ -function Input(name, value, label) { +function Input(name, value, options) { this.name = name; this.value = value; - this.label = label; + this.options = options; + this.label = options && options.label || name; }; Input.prototype.bootstrapify = function(input) { @@ -36,6 +37,16 @@ NumberInput.prototype.render = function() { return this.bootstrapify($('')); }; +function SoapyGainInput() { + Input.apply(this, arguments); +} + +SoapyGainInput.prototype = new Input(); + +SoapyGainInput.prototype.render = function(){ + return this.bootstrapify($('
Soapy gain settings go here
')); +}; + function ProfileInput() { Input.apply(this, arguments); }; @@ -94,37 +105,49 @@ SdrDevice.prototype.getMappings = function() { return { "name": { constructor: TextInput, - label: "Name", + inputOptions: { + label: "Name" + }, initialValue: "", includeInDefault: true }, "type": { constructor: TextInput, - label: "Type", + inputOptions: { + label: "Type" + }, initialValue: '', includeInDefault: true }, "ppm": { constructor: NumberInput, - label: "PPM", + inputOptions: { + label: "PPM" + }, initialValue: 0 }, "profiles": { constructor: ProfileInput, - label: "Profiles", + inputOptions: { + label: "Profiles" + }, initialValue: [], includeInDefault: true, position: 100 }, "scheduler": { constructor: SchedulerInput, - label: "Scheduler", + inputOptions: { + label: "Scheduler", + }, initialValue: {}, position: 101 }, "rf_gain": { constructor: TextInput, - label: "Gain", + inputOptions: { + label: "Gain", + }, initialValue: 0 } }; @@ -140,11 +163,6 @@ SdrDevice.prototype.getInputClass = function(key) { return mapping && mapping.constructor || TextInput; }; -SdrDevice.prototype.getLabel = function(key) { - var mapping = this.getMapping(key); - return mapping && mapping.label || key; -}; - SdrDevice.prototype.getInitialValue = function(key) { var mapping = this.getMapping(key); return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false; @@ -155,6 +173,16 @@ SdrDevice.prototype.getPosition = function(key) { return mapping && mapping.position || 10; }; +SdrDevice.prototype.getInputOptions = function(key) { + var mapping = this.getMapping(key); + return mapping && mapping.inputOptions || {}; +}; + +SdrDevice.prototype.getLabel = function(key) { + var options = this.getInputOptions(key); + return options && options.label || key; +}; + SdrDevice.prototype.render = function() { var self = this; self.el.empty(); @@ -164,7 +192,7 @@ SdrDevice.prototype.render = function() { }).forEach(function(key){ var value = data[key]; var inputClass = self.getInputClass(key); - var input = new inputClass(key, value, self.getLabel(key)); + var input = new inputClass(key, value, self.getInputOptions(key)); self.inputs[key] = input; self.el.append(input.render()); }); @@ -190,7 +218,7 @@ SdrDevice.prototype.renderFieldSelector = function() { '
'; }; -RtlSdrDevice = function(el, data) { +RtlSdrDevice = function() { SdrDevice.apply(this, arguments); }; @@ -202,14 +230,58 @@ RtlSdrDevice.prototype.getMappings = function() { return $.extend(new Object(), mappings, { "device": { constructor: TextInput, - label: "Serial Number", + inputOptions:{ + label: "Serial number" + }, initialValue: "" } }); }; +SoapySdrDevice = function() { + SdrDevice.apply(this, arguments); +}; + +SoapySdrDevice.prototype = Object.create(SdrDevice.prototype); +SoapySdrDevice.prototype.constructor = SoapySdrDevice; + +SoapySdrDevice.prototype.getMappings = function() { + var mappings = SdrDevice.prototype.getMappings.apply(this, arguments); + return $.extend(new Object(), mappings, { + "device": { + constructor: TextInput, + inputOptions:{ + label: "Soapy device selector" + }, + initialValue: "" + } + }); +}; + +SdrplaySdrDevice = function() { + SoapySdrDevice.apply(this, arguments); +}; + +SdrplaySdrDevice.prototype = Object.create(SoapySdrDevice.prototype); +SdrplaySdrDevice.prototype.constructor = SdrplaySdrDevice; + +SdrplaySdrDevice.prototype.getMappings = function() { + var mappings = SoapySdrDevice.prototype.getMappings.apply(this, arguments); + return $.extend(new Object(), mappings, { + "rf_gain": { + constructor: SoapyGainInput, + initialValue: 0, + inputOptions: { + label: "Gain", + gains: ['RFGR', 'IFGR'] + } + } + }); +}; + SdrDevice.types = { - 'rtl_sdr': RtlSdrDevice + 'rtl_sdr': RtlSdrDevice, + 'sdrplay': SdrplaySdrDevice }; $.fn.sdrdevice = function() { From 1299f5e9ccbf317cf5d0a47d3ebc1d258a74d73a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Fri, 22 May 2020 21:25:22 +0200 Subject: [PATCH 339/475] update csdr in docker to the latest version --- docker/scripts/install-dependencies.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index cfd87df..1b1c87b 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -19,7 +19,7 @@ function cmakebuild() { cd /tmp STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools alsa-lib" -BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc alsa-lib-dev linux-headers" +BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc alsa-lib-dev linux-headers pkgconf" apk add --no-cache $STATIC_PACKAGES apk add --no-cache --virtual .build-deps $BUILD_PACKAGES @@ -35,8 +35,10 @@ cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d git clone https://github.com/jketterl/csdr.git cd csdr -git checkout 973de7b471d043a3434f9eb8b56e38fa281d4623 -CSDR_PACKAGE_BUILD=1 make +git checkout 5c8dfdea5fa6e87fa2e9f754be883086fc5a2bfb +autoreconf -i +./configure +make make install cd .. rm -rf csdr From cf4f1dce323a0ac340462bf66badd448853ec220 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 23 May 2020 18:06:46 +0200 Subject: [PATCH 340/475] rebuild docker containers with debian, stage 1: base --- docker/Dockerfiles/Dockerfile-base | 12 +- docker/scripts/direwolf-1.5.patch | 241 ------------------------- docker/scripts/install-dependencies.sh | 36 ++-- docker/scripts/js8call-hamlib.patch | 43 +++++ 4 files changed, 67 insertions(+), 265 deletions(-) delete mode 100644 docker/scripts/direwolf-1.5.patch create mode 100644 docker/scripts/js8call-hamlib.patch diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base index c065768..3ea8aae 100644 --- a/docker/Dockerfiles/Dockerfile-base +++ b/docker/Dockerfiles/Dockerfile-base @@ -1,17 +1,11 @@ -FROM alpine:3.10 +FROM debian:buster-slim -RUN apk add --no-cache bash - -RUN ln -s /usr/local/lib /usr/local/lib64 - -ADD docker/scripts/direwolf-1.5.patch / +ADD docker/scripts/js8call-hamlib.patch / ADD docker/scripts/install-dependencies.sh / RUN /install-dependencies.sh && \ rm /install-dependencies.sh -WORKDIR /opt/openwebrx - VOLUME /etc/openwebrx -ENTRYPOINT [ "/opt/openwebrx/docker/scripts/run.sh" ] +ENTRYPOINT [ "/usr/bin/openwebrx" ] EXPOSE 8073 diff --git a/docker/scripts/direwolf-1.5.patch b/docker/scripts/direwolf-1.5.patch deleted file mode 100644 index 4a63915..0000000 --- a/docker/scripts/direwolf-1.5.patch +++ /dev/null @@ -1,241 +0,0 @@ -diff --git a/Makefile.linux b/Makefile.linux -index 5010833..3f61de9 100644 ---- a/Makefile.linux -+++ b/Makefile.linux -@@ -585,102 +585,102 @@ install : $(APPS) direwolf.conf tocalls.txt symbols-new.txt symbolsX.txt dw-icon - # Applications, not installed with package manager, normally go in /usr/local/bin. - # /usr/bin is used instead when installing from .DEB or .RPM package. - # -- $(INSTALL) -D --mode=755 direwolf $(DESTDIR)/bin/direwolf -- $(INSTALL) -D --mode=755 decode_aprs $(DESTDIR)/bin/decode_aprs -- $(INSTALL) -D --mode=755 text2tt $(DESTDIR)/bin/text2tt -- $(INSTALL) -D --mode=755 tt2text $(DESTDIR)/bin/tt2text -- $(INSTALL) -D --mode=755 ll2utm $(DESTDIR)/bin/ll2utm -- $(INSTALL) -D --mode=755 utm2ll $(DESTDIR)/bin/utm2ll -- $(INSTALL) -D --mode=755 aclients $(DESTDIR)/bin/aclients -- $(INSTALL) -D --mode=755 log2gpx $(DESTDIR)/bin/log2gpx -- $(INSTALL) -D --mode=755 gen_packets $(DESTDIR)/bin/gen_packets -- $(INSTALL) -D --mode=755 atest $(DESTDIR)/bin/atest -- $(INSTALL) -D --mode=755 ttcalc $(DESTDIR)/bin/ttcalc -- $(INSTALL) -D --mode=755 kissutil $(DESTDIR)/bin/kissutil -- $(INSTALL) -D --mode=755 cm108 $(DESTDIR)/bin/cm108 -- $(INSTALL) -D --mode=755 dwespeak.sh $(DESTDIR)/bin/dwspeak.sh -+ $(INSTALL) -D -m=755 direwolf $(DESTDIR)/bin/direwolf -+ $(INSTALL) -D -m=755 decode_aprs $(DESTDIR)/bin/decode_aprs -+ $(INSTALL) -D -m=755 text2tt $(DESTDIR)/bin/text2tt -+ $(INSTALL) -D -m=755 tt2text $(DESTDIR)/bin/tt2text -+ $(INSTALL) -D -m=755 ll2utm $(DESTDIR)/bin/ll2utm -+ $(INSTALL) -D -m=755 utm2ll $(DESTDIR)/bin/utm2ll -+ $(INSTALL) -D -m=755 aclients $(DESTDIR)/bin/aclients -+ $(INSTALL) -D -m=755 log2gpx $(DESTDIR)/bin/log2gpx -+ $(INSTALL) -D -m=755 gen_packets $(DESTDIR)/bin/gen_packets -+ $(INSTALL) -D -m=755 atest $(DESTDIR)/bin/atest -+ $(INSTALL) -D -m=755 ttcalc $(DESTDIR)/bin/ttcalc -+ $(INSTALL) -D -m=755 kissutil $(DESTDIR)/bin/kissutil -+ $(INSTALL) -D -m=755 cm108 $(DESTDIR)/bin/cm108 -+ $(INSTALL) -D -m=755 dwespeak.sh $(DESTDIR)/bin/dwspeak.sh - # - # Telemetry Toolkit executables. Other .conf and .txt files will go into doc directory. - # -- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-balloon.pl $(DESTDIR)/bin/telem-balloon.pl -- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-bits.pl $(DESTDIR)/bin/telem-bits.pl -- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-data.pl $(DESTDIR)/bin/telem-data.pl -- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-data91.pl $(DESTDIR)/bin/telem-data91.pl -- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-eqns.pl $(DESTDIR)/bin/telem-eqns.pl -- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-parm.pl $(DESTDIR)/bin/telem-parm.pl -- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-seq.sh $(DESTDIR)/bin/telem-seq.sh -- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-unit.pl $(DESTDIR)/bin/telem-unit.pl -- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-volts.py $(DESTDIR)/bin/telem-volts.py -+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-balloon.pl $(DESTDIR)/bin/telem-balloon.pl -+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-bits.pl $(DESTDIR)/bin/telem-bits.pl -+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-data.pl $(DESTDIR)/bin/telem-data.pl -+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-data91.pl $(DESTDIR)/bin/telem-data91.pl -+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-eqns.pl $(DESTDIR)/bin/telem-eqns.pl -+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-parm.pl $(DESTDIR)/bin/telem-parm.pl -+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-seq.sh $(DESTDIR)/bin/telem-seq.sh -+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-unit.pl $(DESTDIR)/bin/telem-unit.pl -+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-volts.py $(DESTDIR)/bin/telem-volts.py - # - # Misc. data such as "tocall" to system mapping. - # -- $(INSTALL) -D --mode=644 tocalls.txt $(DESTDIR)/share/direwolf/tocalls.txt -- $(INSTALL) -D --mode=644 symbols-new.txt $(DESTDIR)/share/direwolf/symbols-new.txt -- $(INSTALL) -D --mode=644 symbolsX.txt $(DESTDIR)/share/direwolf/symbolsX.txt -+ $(INSTALL) -D -m=644 tocalls.txt $(DESTDIR)/share/direwolf/tocalls.txt -+ $(INSTALL) -D -m=644 symbols-new.txt $(DESTDIR)/share/direwolf/symbols-new.txt -+ $(INSTALL) -D -m=644 symbolsX.txt $(DESTDIR)/share/direwolf/symbolsX.txt - # - # For desktop icon. - # -- $(INSTALL) -D --mode=644 dw-icon.png $(DESTDIR)/share/direwolf/pixmaps/dw-icon.png -- $(INSTALL) -D --mode=644 direwolf.desktop $(DESTDIR)/share/applications/direwolf.desktop -+ $(INSTALL) -D -m=644 dw-icon.png $(DESTDIR)/share/direwolf/pixmaps/dw-icon.png -+ $(INSTALL) -D -m=644 direwolf.desktop $(DESTDIR)/share/applications/direwolf.desktop - # - # Documentation. Various plain text files and PDF. - # -- $(INSTALL) -D --mode=644 CHANGES.md $(DESTDIR)/share/doc/direwolf/CHANGES.md -- $(INSTALL) -D --mode=644 LICENSE-dire-wolf.txt $(DESTDIR)/share/doc/direwolf/LICENSE-dire-wolf.txt -- $(INSTALL) -D --mode=644 LICENSE-other.txt $(DESTDIR)/share/doc/direwolf/LICENSE-other.txt -+ $(INSTALL) -D -m=644 CHANGES.md $(DESTDIR)/share/doc/direwolf/CHANGES.md -+ $(INSTALL) -D -m=644 LICENSE-dire-wolf.txt $(DESTDIR)/share/doc/direwolf/LICENSE-dire-wolf.txt -+ $(INSTALL) -D -m=644 LICENSE-other.txt $(DESTDIR)/share/doc/direwolf/LICENSE-other.txt - # - # ./README.md is an overview for the project main page. - # Maybe we could stick it in some other place. - # doc/README.md contains an overview of the PDF file contents and is more useful here. - # -- $(INSTALL) -D --mode=644 doc/README.md $(DESTDIR)/share/doc/direwolf/README.md -- $(INSTALL) -D --mode=644 doc/2400-4800-PSK-for-APRS-Packet-Radio.pdf $(DESTDIR)/share/doc/direwolf/2400-4800-PSK-for-APRS-Packet-Radio.pdf -- $(INSTALL) -D --mode=644 doc/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf -- $(INSTALL) -D --mode=644 doc/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf -- $(INSTALL) -D --mode=644 doc/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf $(DESTDIR)/share/doc/direwolf/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf -- $(INSTALL) -D --mode=644 doc/APRS-Telemetry-Toolkit.pdf $(DESTDIR)/share/doc/direwolf/APRS-Telemetry-Toolkit.pdf -- $(INSTALL) -D --mode=644 doc/APRStt-Implementation-Notes.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Implementation-Notes.pdf -- $(INSTALL) -D --mode=644 doc/APRStt-interface-for-SARTrack.pdf $(DESTDIR)/share/doc/direwolf/APRStt-interface-for-SARTrack.pdf -- $(INSTALL) -D --mode=644 doc/APRStt-Listening-Example.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Listening-Example.pdf -- $(INSTALL) -D --mode=644 doc/Bluetooth-KISS-TNC.pdf $(DESTDIR)/share/doc/direwolf/Bluetooth-KISS-TNC.pdf -- $(INSTALL) -D --mode=644 doc/Going-beyond-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/Going-beyond-9600-baud.pdf -- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-APRS.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS.pdf -- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-APRS-Tracker.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS-Tracker.pdf -- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-SDR-IGate.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-SDR-IGate.pdf -- $(INSTALL) -D --mode=644 doc/Successful-APRS-IGate-Operation.pdf $(DESTDIR)/share/doc/direwolf/Successful-APRS-IGate-Operation.pdf -- $(INSTALL) -D --mode=644 doc/User-Guide.pdf $(DESTDIR)/share/doc/direwolf/User-Guide.pdf -- $(INSTALL) -D --mode=644 doc/WA8LMF-TNC-Test-CD-Results.pdf $(DESTDIR)/share/doc/direwolf/WA8LMF-TNC-Test-CD-Results.pdf -+ $(INSTALL) -D -m=644 doc/README.md $(DESTDIR)/share/doc/direwolf/README.md -+ $(INSTALL) -D -m=644 doc/2400-4800-PSK-for-APRS-Packet-Radio.pdf $(DESTDIR)/share/doc/direwolf/2400-4800-PSK-for-APRS-Packet-Radio.pdf -+ $(INSTALL) -D -m=644 doc/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf -+ $(INSTALL) -D -m=644 doc/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf -+ $(INSTALL) -D -m=644 doc/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf $(DESTDIR)/share/doc/direwolf/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf -+ $(INSTALL) -D -m=644 doc/APRS-Telemetry-Toolkit.pdf $(DESTDIR)/share/doc/direwolf/APRS-Telemetry-Toolkit.pdf -+ $(INSTALL) -D -m=644 doc/APRStt-Implementation-Notes.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Implementation-Notes.pdf -+ $(INSTALL) -D -m=644 doc/APRStt-interface-for-SARTrack.pdf $(DESTDIR)/share/doc/direwolf/APRStt-interface-for-SARTrack.pdf -+ $(INSTALL) -D -m=644 doc/APRStt-Listening-Example.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Listening-Example.pdf -+ $(INSTALL) -D -m=644 doc/Bluetooth-KISS-TNC.pdf $(DESTDIR)/share/doc/direwolf/Bluetooth-KISS-TNC.pdf -+ $(INSTALL) -D -m=644 doc/Going-beyond-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/Going-beyond-9600-baud.pdf -+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-APRS.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS.pdf -+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-APRS-Tracker.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS-Tracker.pdf -+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-SDR-IGate.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-SDR-IGate.pdf -+ $(INSTALL) -D -m=644 doc/Successful-APRS-IGate-Operation.pdf $(DESTDIR)/share/doc/direwolf/Successful-APRS-IGate-Operation.pdf -+ $(INSTALL) -D -m=644 doc/User-Guide.pdf $(DESTDIR)/share/doc/direwolf/User-Guide.pdf -+ $(INSTALL) -D -m=644 doc/WA8LMF-TNC-Test-CD-Results.pdf $(DESTDIR)/share/doc/direwolf/WA8LMF-TNC-Test-CD-Results.pdf - # - # Various sample config and other files go into examples under the doc directory. - # When building from source, these can be put in home directory with "make install-conf". - # When installed from .DEB or .RPM package, the user will need to copy these to - # the home directory or other desired location. - # -- $(INSTALL) -D --mode=644 direwolf.conf $(DESTDIR)/share/doc/direwolf/examples/direwolf.conf -- $(INSTALL) -D --mode=755 dw-start.sh $(DESTDIR)/share/doc/direwolf/examples/dw-start.sh -- $(INSTALL) -D --mode=644 sdr.conf $(DESTDIR)/share/doc/direwolf/examples/sdr.conf -- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-m0xer-3.txt $(DESTDIR)/share/doc/direwolf/examples/telem-m0xer-3.txt -- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-balloon.conf $(DESTDIR)/share/doc/direwolf/examples/telem-balloon.conf -- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-volts.conf $(DESTDIR)/share/doc/direwolf/examples/telem-volts.conf -+ $(INSTALL) -D -m=644 direwolf.conf $(DESTDIR)/share/doc/direwolf/examples/direwolf.conf -+ $(INSTALL) -D -m=755 dw-start.sh $(DESTDIR)/share/doc/direwolf/examples/dw-start.sh -+ $(INSTALL) -D -m=644 sdr.conf $(DESTDIR)/share/doc/direwolf/examples/sdr.conf -+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-m0xer-3.txt $(DESTDIR)/share/doc/direwolf/examples/telem-m0xer-3.txt -+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-balloon.conf $(DESTDIR)/share/doc/direwolf/examples/telem-balloon.conf -+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-volts.conf $(DESTDIR)/share/doc/direwolf/examples/telem-volts.conf - # - # "man" pages - # -- $(INSTALL) -D --mode=644 man1/aclients.1 $(DESTDIR)/share/man/man1/aclients.1 -- $(INSTALL) -D --mode=644 man1/atest.1 $(DESTDIR)/share/man/man1/atest.1 -- $(INSTALL) -D --mode=644 man1/decode_aprs.1 $(DESTDIR)/share/man/man1/decode_aprs.1 -- $(INSTALL) -D --mode=644 man1/direwolf.1 $(DESTDIR)/share/man/man1/direwolf.1 -- $(INSTALL) -D --mode=644 man1/gen_packets.1 $(DESTDIR)/share/man/man1/gen_packets.1 -- $(INSTALL) -D --mode=644 man1/kissutil.1 $(DESTDIR)/share/man/man1/kissutil.1 -- $(INSTALL) -D --mode=644 man1/ll2utm.1 $(DESTDIR)/share/man/man1/ll2utm.1 -- $(INSTALL) -D --mode=644 man1/log2gpx.1 $(DESTDIR)/share/man/man1/log2gpx.1 -- $(INSTALL) -D --mode=644 man1/text2tt.1 $(DESTDIR)/share/man/man1/text2tt.1 -- $(INSTALL) -D --mode=644 man1/tt2text.1 $(DESTDIR)/share/man/man1/tt2text.1 -- $(INSTALL) -D --mode=644 man1/utm2ll.1 $(DESTDIR)/share/man/man1/utm2ll.1 -+ $(INSTALL) -D -m=644 man1/aclients.1 $(DESTDIR)/share/man/man1/aclients.1 -+ $(INSTALL) -D -m=644 man1/atest.1 $(DESTDIR)/share/man/man1/atest.1 -+ $(INSTALL) -D -m=644 man1/decode_aprs.1 $(DESTDIR)/share/man/man1/decode_aprs.1 -+ $(INSTALL) -D -m=644 man1/direwolf.1 $(DESTDIR)/share/man/man1/direwolf.1 -+ $(INSTALL) -D -m=644 man1/gen_packets.1 $(DESTDIR)/share/man/man1/gen_packets.1 -+ $(INSTALL) -D -m=644 man1/kissutil.1 $(DESTDIR)/share/man/man1/kissutil.1 -+ $(INSTALL) -D -m=644 man1/ll2utm.1 $(DESTDIR)/share/man/man1/ll2utm.1 -+ $(INSTALL) -D -m=644 man1/log2gpx.1 $(DESTDIR)/share/man/man1/log2gpx.1 -+ $(INSTALL) -D -m=644 man1/text2tt.1 $(DESTDIR)/share/man/man1/text2tt.1 -+ $(INSTALL) -D -m=644 man1/tt2text.1 $(DESTDIR)/share/man/man1/tt2text.1 -+ $(INSTALL) -D -m=644 man1/utm2ll.1 $(DESTDIR)/share/man/man1/utm2ll.1 - # - # Set group and mode of HID devices corresponding to C-Media USB Audio adapters. - # This will allow us to use the CM108/CM119 GPIO pins for PTT. - # -- $(INSTALL) -D --mode=644 99-direwolf-cmedia.rules /etc/udev/rules.d/99-direwolf-cmedia.rules -+ $(INSTALL) -D -m=644 99-direwolf-cmedia.rules /etc/udev/rules.d/99-direwolf-cmedia.rules - # - @echo " " - @echo "If this is your first install, not an upgrade, type this to put a copy" -diff --git a/cdigipeater.c b/cdigipeater.c -index 9c40d95..94112e9 100644 ---- a/cdigipeater.c -+++ b/cdigipeater.c -@@ -49,7 +49,7 @@ - #include - #include /* for isdigit, isupper */ - #include "regex.h" --#include -+#include - - #include "ax25_pad.h" - #include "cdigipeater.h" -diff --git a/decode_aprs.c b/decode_aprs.c -index 35c186b..a620cb3 100644 ---- a/decode_aprs.c -+++ b/decode_aprs.c -@@ -3872,11 +3872,7 @@ static void decode_tocall (decode_aprs_t *A, char *dest) - * models before getting to the more generic APY. - */ - --#if defined(__WIN32__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__) - qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), tocall_cmp); --#else -- qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), (__compar_fn_t)tocall_cmp); --#endif - } - else { - if ( ! A->g_quiet) { -diff --git a/digipeater.c b/digipeater.c -index 36970d7..5195582 100644 ---- a/digipeater.c -+++ b/digipeater.c -@@ -62,7 +62,7 @@ - #include - #include /* for isdigit, isupper */ - #include "regex.h" --#include -+#include - - #include "ax25_pad.h" - #include "digipeater.h" -diff --git a/direwolf.h b/direwolf.h -index 514bcc5..52f5ae9 100644 ---- a/direwolf.h -+++ b/direwolf.h -@@ -274,7 +274,7 @@ char *strtok_r(char *str, const char *delim, char **saveptr); - char *strcasestr(const char *S, const char *FIND); - - --#if defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__) -+#if 1 - - // strlcpy and strlcat should be in string.h and the C library. - -diff --git a/multi_modem.c b/multi_modem.c -index 5d96c79..24261b9 100644 ---- a/multi_modem.c -+++ b/multi_modem.c -@@ -80,7 +80,7 @@ - #include - #include - #include --#include -+#include - - #include "ax25_pad.h" - #include "textcolor.h" diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 1b1c87b..9f18cda 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -18,11 +18,11 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools alsa-lib" -BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc alsa-lib-dev linux-headers pkgconf" +STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0.0 libqt5serialport5 libqt5multimedia5-plugins libqt5widgets5 libqt5sql5-sqlite libqt5core5a libqt5gui5 libqt5multimedia5 libqt5network5 libqt5printsupport5 libqt5serialport5 libqt5sql5 libqt5widgets5 libreadline7 libgfortran4 libgomp1 libasound2 libudev1 libhamlib2 libhamlib2++c2" +BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake ca-certificates make gcc g++ liblapack-dev autoconf automake libtool texinfo gfortran libusb-1.0.0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev pkg-config libudev-dev libhamlib-dev libhamlib++-dev" -apk add --no-cache $STATIC_PACKAGES -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +apt-get update +apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/jketterl/js8py.git pushd js8py @@ -52,21 +52,26 @@ cmakebuild digiham 95206501be89b38d0267bf6c29a6898e7c65656f git clone https://github.com/f4exb/dsd.git cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7 -git clone https://github.com/Hamlib/Hamlib.git -pushd Hamlib -git checkout 301ebb92eaa538dfa75c06821f46715f40dd7673 -./bootstrap -./configure -make -make install -popd -rm -rf Hamlib +#git clone https://github.com/Hamlib/Hamlib.git +#pushd Hamlib +#git checkout 301ebb92eaa538dfa75c06821f46715f40dd7673 +#./bootstrap +#./configure +#make +#ake install +#popd +#rm -rf Hamlib JS8CALL_VERSION=2.1.1 JS8CALL_DIR=js8call-${JS8CALL_VERSION} JS8CALL_TGZ=${JS8CALL_DIR}.tgz wget http://files.js8call.com/${JS8CALL_VERSION}/${JS8CALL_TGZ} tar xfz ${JS8CALL_TGZ} +#cp ${JS8CALL_DIR}/CMakeLists.txt ./CMakeLists.orig.txt +#sed "s/set (hamlib_STATIC 1)/set (hamlib_STATIC 0)/" < ./CMakeLists.orig.txt > ${JS8CALL_DIR}/CMakeLists.txt +#rm ./CMakeLists.orig.txt +patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch +rm /js8call-hamlib.patch CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_LEGACY_HAMLIB" cmakebuild ${JS8CALL_DIR} rm ${JS8CALL_TGZ} @@ -79,7 +84,6 @@ rm ${WSJT_TGZ} git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git cd direwolf -patch -Np1 < /direwolf-1.5.patch make make install cd .. @@ -90,4 +94,6 @@ pushd /opt/aprs-symbols git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802 popd -apk del .build-deps +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/js8call-hamlib.patch b/docker/scripts/js8call-hamlib.patch new file mode 100644 index 0000000..68c1e70 --- /dev/null +++ b/docker/scripts/js8call-hamlib.patch @@ -0,0 +1,43 @@ +diff -ur js8call-2.1.1-orig/CMake/Modules/Findhamlib.cmake js8call-2.1.1/CMake/Modules/Findhamlib.cmake +--- js8call-2.1.1-orig/CMake/Modules/Findhamlib.cmake 2020-05-23 15:38:20.730349612 +0000 ++++ js8call-2.1.1/CMake/Modules/Findhamlib.cmake 2020-05-23 15:39:28.829772207 +0000 +@@ -78,4 +78,4 @@ + # Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to + # TRUE if all listed variables are TRUE + include (FindPackageHandleStandardArgs) +-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS) ++find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES) +diff -ur js8call-2.1.1-orig/CMakeLists.txt js8call-2.1.1/CMakeLists.txt +--- js8call-2.1.1-orig/CMakeLists.txt 2020-05-23 15:38:20.730349612 +0000 ++++ js8call-2.1.1/CMakeLists.txt 2020-05-23 15:52:46.103389553 +0000 +@@ -683,7 +683,7 @@ + # + # libhamlib setup + # +-set (hamlib_STATIC 1) ++set (hamlib_STATIC 0) + find_package (hamlib 3 REQUIRED) + find_program (RIGCTL_EXE rigctl) + find_program (RIGCTLD_EXE rigctld) +@@ -1106,20 +1106,6 @@ + BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime + ) + +-install (PROGRAMS +- ${RIGCTL_EXE} +- DESTINATION ${CMAKE_INSTALL_BINDIR} +- #COMPONENT runtime +- RENAME rigctl-local${CMAKE_EXECUTABLE_SUFFIX} +- ) +- +-install (PROGRAMS +- ${RIGCTLD_EXE} +- DESTINATION ${CMAKE_INSTALL_BINDIR} +- #COMPONENT runtime +- RENAME rigctld-local${CMAKE_EXECUTABLE_SUFFIX} +- ) +- + install (FILES + README + COPYING +Only in js8call-2.1.1/: hamlib.patch From d9292587ec12dddc9c5fc52d6a21c7d4f872bf99 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 23 May 2020 19:59:31 +0200 Subject: [PATCH 341/475] part 2: all the image builds --- docker/scripts/install-connectors.sh | 10 ++++++---- docker/scripts/install-dependencies-airspy.sh | 12 +++++++----- docker/scripts/install-dependencies-hackrf.sh | 12 +++++++----- docker/scripts/install-dependencies-limesdr.sh | 12 +++++++----- docker/scripts/install-dependencies-perseus.sh | 12 +++++++----- docker/scripts/install-dependencies-plutosdr.sh | 12 +++++++----- docker/scripts/install-dependencies-rtlsdr-soapy.sh | 12 +++++++----- docker/scripts/install-dependencies-rtlsdr.sh | 12 +++++++----- docker/scripts/install-dependencies-sdrplay.sh | 10 ++++++---- docker/scripts/install-dependencies-soapyremote.sh | 12 +++++++----- docker/scripts/install-dependencies-soapysdr.sh | 10 ++++++---- docker/scripts/install-dependencies.sh | 4 ++-- 12 files changed, 76 insertions(+), 54 deletions(-) diff --git a/docker/scripts/install-connectors.sh b/docker/scripts/install-connectors.sh index b848e7a..5b92f56 100755 --- a/docker/scripts/install-connectors.sh +++ b/docker/scripts/install-connectors.sh @@ -18,12 +18,14 @@ function cmakebuild() { cd /tmp -BUILD_PACKAGES="git cmake make gcc g++ musl-dev" - -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +BUILD_PACKAGES="git cmake make gcc g++" +apt-get update +apt-get -y install $BUILD_PACKAGES git clone https://github.com/jketterl/owrx_connector.git cmakebuild owrx_connector 3ffbdcccbe39a5675fd539d959e64885e521eae6 -apk del .build-deps +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-airspy.sh b/docker/scripts/install-dependencies-airspy.sh index a7813d3..4b6fb50 100755 --- a/docker/scripts/install-dependencies-airspy.sh +++ b/docker/scripts/install-dependencies-airspy.sh @@ -18,11 +18,11 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="libusb" -BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" +STATIC_PACKAGES="libusb-1.0-0" +BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config" -apk add --no-cache $STATIC_PACKAGES -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +apt-get update +apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/airspy/airspyone_host.git cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3 @@ -36,4 +36,6 @@ cmakebuild airspyhf 613852a2bb64af42690bf9be2201826af69a9475 git clone https://github.com/pothosware/SoapyAirspyHF.git cmakebuild SoapyAirspyHF 81ca737bb044dd930a9de738bced1e4915491f1b -apk del .build-deps +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-hackrf.sh b/docker/scripts/install-dependencies-hackrf.sh index 0e56119..f0af991 100755 --- a/docker/scripts/install-dependencies-hackrf.sh +++ b/docker/scripts/install-dependencies-hackrf.sh @@ -18,11 +18,11 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="libusb fftw udev" -BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev fftw-dev" +STATIC_PACKAGES="libusb-1.0-0 libfftw3-3 udev" +BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config" -apk add --no-cache $STATIC_PACKAGES -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +apt-get update +apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/mossmann/hackrf.git cd hackrf @@ -31,4 +31,6 @@ cmakebuild host cd .. rm -rf hackrf -apk del .build-deps +SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-limesdr.sh b/docker/scripts/install-dependencies-limesdr.sh index 0e5cef5..cc3cbb1 100755 --- a/docker/scripts/install-dependencies-limesdr.sh +++ b/docker/scripts/install-dependencies-limesdr.sh @@ -4,11 +4,11 @@ export MAKEFLAGS="-j4" cd /tmp -STATIC_PACKAGES="libusb" -BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" +STATIC_PACKAGES="libusb-1.0-0" +BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++" -apk add --no-cache $STATIC_PACKAGES -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +apt-get update +apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/myriadrf/LimeSuite.git cd LimeSuite @@ -21,4 +21,6 @@ make install cd ../.. rm -rf LimeSuite -apk del .build-deps +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-perseus.sh b/docker/scripts/install-dependencies-perseus.sh index 62a0a71..e0463e0 100755 --- a/docker/scripts/install-dependencies-perseus.sh +++ b/docker/scripts/install-dependencies-perseus.sh @@ -4,11 +4,11 @@ export MAKEFLAGS="-j4" cd /tmp -STATIC_PACKAGES="libusb udev" -BUILD_PACKAGES="git make gcc autoconf automake libtool musl-dev libusb-dev shadow vim" +STATIC_PACKAGES="libusb-1.0-0 libudev1" +BUILD_PACKAGES="git make gcc autoconf automake libtool libusb-1.0-0-dev xxd" -apk add --no-cache $STATIC_PACKAGES -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +apt-get update +apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/Microtelecom/libperseus-sdr.git cd libperseus-sdr @@ -21,4 +21,6 @@ ldconfig /etc/ld.so.conf.d cd .. rm -rf libperseus-sdr -apk del .build-deps +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-plutosdr.sh b/docker/scripts/install-dependencies-plutosdr.sh index 50cd353..290ff1f 100755 --- a/docker/scripts/install-dependencies-plutosdr.sh +++ b/docker/scripts/install-dependencies-plutosdr.sh @@ -18,11 +18,11 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="libusb libxml2" -BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers libxml2-dev flex bison" +STATIC_PACKAGES="libusb-1.0-0 libxml2" +BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ libxml2-dev flex bison" -apk add --no-cache $STATIC_PACKAGES -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +apt-get update +apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/analogdevicesinc/libiio.git cmakebuild libiio 5f5af2e417129ad8f4e05fc5c1b730f0694dca12 -DCMAKE_INSTALL_PREFIX=/usr/local @@ -33,4 +33,6 @@ cmakebuild libad9361-iio 8ac95f3325c18c2e34cd9cfd49c7b63d69a0a9d2 git clone https://github.com/pothosware/SoapyPlutoSDR.git cmakebuild SoapyPlutoSDR c88b7f5bac1e5785f212f9a7c6ce8fef64eb719e -apk del .build-deps +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-rtlsdr-soapy.sh b/docker/scripts/install-dependencies-rtlsdr-soapy.sh index 8a70417..a5dc03d 100755 --- a/docker/scripts/install-dependencies-rtlsdr-soapy.sh +++ b/docker/scripts/install-dependencies-rtlsdr-soapy.sh @@ -18,11 +18,11 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="libusb" -BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" +STATIC_PACKAGES="libusb-1.0-0" +BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config" -apk add --no-cache $STATIC_PACKAGES -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +apt-get update +apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/osmocom/rtl-sdr.git cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320 @@ -30,4 +30,6 @@ cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320 git clone https://github.com/pothosware/SoapyRTLSDR.git cmakebuild SoapyRTLSDR 8ba18f17d64005e43ff2a4e46611f8c710b05007 -apk del .build-deps +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-rtlsdr.sh b/docker/scripts/install-dependencies-rtlsdr.sh index 9c7370b..dee4df4 100755 --- a/docker/scripts/install-dependencies-rtlsdr.sh +++ b/docker/scripts/install-dependencies-rtlsdr.sh @@ -18,13 +18,15 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="libusb" -BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" +STATIC_PACKAGES="libusb-1.0.0" +BUILD_PACKAGES="git libusb-1.0.0-dev cmake make gcc g++ pkg-config" -apk add --no-cache $STATIC_PACKAGES -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +apt-get update +apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/osmocom/rtl-sdr.git cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320 -apk del .build-deps +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-sdrplay.sh b/docker/scripts/install-dependencies-sdrplay.sh index d127602..c7071ec 100755 --- a/docker/scripts/install-dependencies-sdrplay.sh +++ b/docker/scripts/install-dependencies-sdrplay.sh @@ -18,11 +18,11 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="libusb udev" +STATIC_PACKAGES="libusb-1.0.0 libudev1" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev" -apk add --no-cache $STATIC_PACKAGES -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +apt-get update +apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES ARCH=$(uname -m) @@ -51,4 +51,6 @@ rm $BINARY git clone https://github.com/pothosware/SoapySDRPlay.git cmakebuild SoapySDRPlay 14ec39e4ff0dab7ae7fdf1afbbd2d28b49b0ffae -apk del .build-deps +SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-soapyremote.sh b/docker/scripts/install-dependencies-soapyremote.sh index 9b95f09..b1b3800 100755 --- a/docker/scripts/install-dependencies-soapyremote.sh +++ b/docker/scripts/install-dependencies-soapyremote.sh @@ -18,13 +18,15 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="avahi" -BUILD_PACKAGES="git cmake make gcc musl-dev g++ linux-headers avahi-dev" +STATIC_PACKAGES="avahi-daemon libavahi-client3" +BUILD_PACKAGES="git cmake make gcc g++ libavahi-client-dev" -apk add --no-cache $STATIC_PACKAGES -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +apt-get update +apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/pothosware/SoapyRemote.git cmakebuild SoapyRemote 6d9bd820da470cfe7b27b2e6946af93cfece448f -apk del .build-deps +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-soapysdr.sh b/docker/scripts/install-dependencies-soapysdr.sh index bfbcd66..da77c76 100755 --- a/docker/scripts/install-dependencies-soapysdr.sh +++ b/docker/scripts/install-dependencies-soapysdr.sh @@ -18,13 +18,15 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="udev" +STATIC_PACKAGES="libudev1" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++" -apk add --no-cache $STATIC_PACKAGES -apk add --no-cache --virtual .build-deps $BUILD_PACKAGES +apt-get update +apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/pothosware/SoapySDR cmakebuild SoapySDR f722f9ce5b629c3c44401a9bf628b3f8e67a9695 -apk del .build-deps +SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 9f18cda..3285ce3 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -18,8 +18,8 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0.0 libqt5serialport5 libqt5multimedia5-plugins libqt5widgets5 libqt5sql5-sqlite libqt5core5a libqt5gui5 libqt5multimedia5 libqt5network5 libqt5printsupport5 libqt5serialport5 libqt5sql5 libqt5widgets5 libreadline7 libgfortran4 libgomp1 libasound2 libudev1 libhamlib2 libhamlib2++c2" -BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake ca-certificates make gcc g++ liblapack-dev autoconf automake libtool texinfo gfortran libusb-1.0.0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev pkg-config libudev-dev libhamlib-dev libhamlib++-dev" +STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5serialport5 libqt5multimedia5-plugins libqt5widgets5 libqt5sql5-sqlite libqt5core5a libqt5gui5 libqt5multimedia5 libqt5network5 libqt5printsupport5 libqt5serialport5 libqt5sql5 libqt5widgets5 libreadline7 libgfortran4 libgomp1 libasound2 libudev1 libhamlib2" +BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake ca-certificates make gcc g++ liblapack-dev autoconf automake libtool texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev pkg-config libudev-dev libhamlib-dev" apt-get update apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES From 6cac3b4d39fadbdfd809e2b37b7a03bf864bbc45 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 23 May 2020 22:51:46 +0200 Subject: [PATCH 342/475] restore startup --- docker/Dockerfiles/Dockerfile-base | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base index 3ea8aae..2ac1cd4 100644 --- a/docker/Dockerfiles/Dockerfile-base +++ b/docker/Dockerfiles/Dockerfile-base @@ -5,7 +5,9 @@ ADD docker/scripts/install-dependencies.sh / RUN /install-dependencies.sh && \ rm /install-dependencies.sh +WORKDIR /opt/openwebrx + VOLUME /etc/openwebrx -ENTRYPOINT [ "/usr/bin/openwebrx" ] +ENTRYPOINT [ "/opt/openwebrx/docker/scripts/run.sh" ] EXPOSE 8073 From 1fedd0e50f2b930a4c79dd2be5a061463e855a84 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 23 May 2020 22:52:22 +0200 Subject: [PATCH 343/475] limesdr requires libatomic --- docker/scripts/install-dependencies-limesdr.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/install-dependencies-limesdr.sh b/docker/scripts/install-dependencies-limesdr.sh index cc3cbb1..bbafb9c 100755 --- a/docker/scripts/install-dependencies-limesdr.sh +++ b/docker/scripts/install-dependencies-limesdr.sh @@ -4,7 +4,7 @@ export MAKEFLAGS="-j4" cd /tmp -STATIC_PACKAGES="libusb-1.0-0" +STATIC_PACKAGES="libusb-1.0-0 libatomic1" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++" apt-get update From 5377087848d1bb0a9a1dd7c26f1319098f8a7300 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 23 May 2020 22:53:12 +0200 Subject: [PATCH 344/475] don't install unnecessary dependencies --- docker/scripts/install-connectors.sh | 2 +- docker/scripts/install-dependencies-airspy.sh | 2 +- docker/scripts/install-dependencies-hackrf.sh | 2 +- .../scripts/install-dependencies-limesdr.sh | 2 +- .../scripts/install-dependencies-perseus.sh | 2 +- .../scripts/install-dependencies-plutosdr.sh | 2 +- .../install-dependencies-rtlsdr-soapy.sh | 2 +- docker/scripts/install-dependencies-rtlsdr.sh | 2 +- .../scripts/install-dependencies-sdrplay.sh | 2 +- .../install-dependencies-soapyremote.sh | 2 +- .../scripts/install-dependencies-soapysdr.sh | 2 +- docker/scripts/install-dependencies.sh | 21 +++++-------------- 12 files changed, 16 insertions(+), 27 deletions(-) diff --git a/docker/scripts/install-connectors.sh b/docker/scripts/install-connectors.sh index 5b92f56..15d61dc 100755 --- a/docker/scripts/install-connectors.sh +++ b/docker/scripts/install-connectors.sh @@ -21,7 +21,7 @@ cd /tmp BUILD_PACKAGES="git cmake make gcc g++" apt-get update -apt-get -y install $BUILD_PACKAGES +apt-get -y install --no-install-recommends $BUILD_PACKAGES git clone https://github.com/jketterl/owrx_connector.git cmakebuild owrx_connector 3ffbdcccbe39a5675fd539d959e64885e521eae6 diff --git a/docker/scripts/install-dependencies-airspy.sh b/docker/scripts/install-dependencies-airspy.sh index 4b6fb50..9927842 100755 --- a/docker/scripts/install-dependencies-airspy.sh +++ b/docker/scripts/install-dependencies-airspy.sh @@ -22,7 +22,7 @@ STATIC_PACKAGES="libusb-1.0-0" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config" apt-get update -apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/airspy/airspyone_host.git cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3 diff --git a/docker/scripts/install-dependencies-hackrf.sh b/docker/scripts/install-dependencies-hackrf.sh index f0af991..9c6d47b 100755 --- a/docker/scripts/install-dependencies-hackrf.sh +++ b/docker/scripts/install-dependencies-hackrf.sh @@ -22,7 +22,7 @@ STATIC_PACKAGES="libusb-1.0-0 libfftw3-3 udev" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config" apt-get update -apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/mossmann/hackrf.git cd hackrf diff --git a/docker/scripts/install-dependencies-limesdr.sh b/docker/scripts/install-dependencies-limesdr.sh index bbafb9c..abfa393 100755 --- a/docker/scripts/install-dependencies-limesdr.sh +++ b/docker/scripts/install-dependencies-limesdr.sh @@ -8,7 +8,7 @@ STATIC_PACKAGES="libusb-1.0-0 libatomic1" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++" apt-get update -apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/myriadrf/LimeSuite.git cd LimeSuite diff --git a/docker/scripts/install-dependencies-perseus.sh b/docker/scripts/install-dependencies-perseus.sh index e0463e0..eba74fe 100755 --- a/docker/scripts/install-dependencies-perseus.sh +++ b/docker/scripts/install-dependencies-perseus.sh @@ -8,7 +8,7 @@ STATIC_PACKAGES="libusb-1.0-0 libudev1" BUILD_PACKAGES="git make gcc autoconf automake libtool libusb-1.0-0-dev xxd" apt-get update -apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/Microtelecom/libperseus-sdr.git cd libperseus-sdr diff --git a/docker/scripts/install-dependencies-plutosdr.sh b/docker/scripts/install-dependencies-plutosdr.sh index 290ff1f..18df91e 100755 --- a/docker/scripts/install-dependencies-plutosdr.sh +++ b/docker/scripts/install-dependencies-plutosdr.sh @@ -22,7 +22,7 @@ STATIC_PACKAGES="libusb-1.0-0 libxml2" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ libxml2-dev flex bison" apt-get update -apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/analogdevicesinc/libiio.git cmakebuild libiio 5f5af2e417129ad8f4e05fc5c1b730f0694dca12 -DCMAKE_INSTALL_PREFIX=/usr/local diff --git a/docker/scripts/install-dependencies-rtlsdr-soapy.sh b/docker/scripts/install-dependencies-rtlsdr-soapy.sh index a5dc03d..b50921d 100755 --- a/docker/scripts/install-dependencies-rtlsdr-soapy.sh +++ b/docker/scripts/install-dependencies-rtlsdr-soapy.sh @@ -22,7 +22,7 @@ STATIC_PACKAGES="libusb-1.0-0" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config" apt-get update -apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/osmocom/rtl-sdr.git cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320 diff --git a/docker/scripts/install-dependencies-rtlsdr.sh b/docker/scripts/install-dependencies-rtlsdr.sh index dee4df4..b26d432 100755 --- a/docker/scripts/install-dependencies-rtlsdr.sh +++ b/docker/scripts/install-dependencies-rtlsdr.sh @@ -22,7 +22,7 @@ STATIC_PACKAGES="libusb-1.0.0" BUILD_PACKAGES="git libusb-1.0.0-dev cmake make gcc g++ pkg-config" apt-get update -apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/osmocom/rtl-sdr.git cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320 diff --git a/docker/scripts/install-dependencies-sdrplay.sh b/docker/scripts/install-dependencies-sdrplay.sh index c7071ec..5e689df 100755 --- a/docker/scripts/install-dependencies-sdrplay.sh +++ b/docker/scripts/install-dependencies-sdrplay.sh @@ -22,7 +22,7 @@ STATIC_PACKAGES="libusb-1.0.0 libudev1" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev" apt-get update -apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES ARCH=$(uname -m) diff --git a/docker/scripts/install-dependencies-soapyremote.sh b/docker/scripts/install-dependencies-soapyremote.sh index b1b3800..cae7c11 100755 --- a/docker/scripts/install-dependencies-soapyremote.sh +++ b/docker/scripts/install-dependencies-soapyremote.sh @@ -22,7 +22,7 @@ STATIC_PACKAGES="avahi-daemon libavahi-client3" BUILD_PACKAGES="git cmake make gcc g++ libavahi-client-dev" apt-get update -apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/pothosware/SoapyRemote.git cmakebuild SoapyRemote 6d9bd820da470cfe7b27b2e6946af93cfece448f diff --git a/docker/scripts/install-dependencies-soapysdr.sh b/docker/scripts/install-dependencies-soapysdr.sh index da77c76..5687d44 100755 --- a/docker/scripts/install-dependencies-soapysdr.sh +++ b/docker/scripts/install-dependencies-soapysdr.sh @@ -22,7 +22,7 @@ STATIC_PACKAGES="libudev1" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++" apt-get update -apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/pothosware/SoapySDR cmakebuild SoapySDR f722f9ce5b629c3c44401a9bf628b3f8e67a9695 diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 3285ce3..cee0f14 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -18,17 +18,18 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5serialport5 libqt5multimedia5-plugins libqt5widgets5 libqt5sql5-sqlite libqt5core5a libqt5gui5 libqt5multimedia5 libqt5network5 libqt5printsupport5 libqt5serialport5 libqt5sql5 libqt5widgets5 libreadline7 libgfortran4 libgomp1 libasound2 libudev1 libhamlib2" -BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake ca-certificates make gcc g++ liblapack-dev autoconf automake libtool texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev pkg-config libudev-dev libhamlib-dev" +STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5serialport5 libqt5multimedia5-plugins libqt5widgets5 libqt5sql5-sqlite libqt5core5a libqt5multimedia5 libqt5network5 libqt5printsupport5 libqt5serialport5 libqt5sql5 libqt5widgets5 libreadline7 libgfortran4 libgomp1 libasound2 libudev1 libhamlib2 ca-certificates" +BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev autoconf automake libtool texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev pkg-config libudev-dev libhamlib-dev patch xsltproc" apt-get update -apt-get -y install $STATIC_PACKAGES $BUILD_PACKAGES +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES git clone https://github.com/jketterl/js8py.git pushd js8py git checkout 888e62be375316882ad2b2ac8e396c3bf857b6fc python3 setup.py install popd +rm -rf js8py git clone https://git.code.sf.net/p/itpp/git itpp cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d @@ -52,24 +53,12 @@ cmakebuild digiham 95206501be89b38d0267bf6c29a6898e7c65656f git clone https://github.com/f4exb/dsd.git cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7 -#git clone https://github.com/Hamlib/Hamlib.git -#pushd Hamlib -#git checkout 301ebb92eaa538dfa75c06821f46715f40dd7673 -#./bootstrap -#./configure -#make -#ake install -#popd -#rm -rf Hamlib - JS8CALL_VERSION=2.1.1 JS8CALL_DIR=js8call-${JS8CALL_VERSION} JS8CALL_TGZ=${JS8CALL_DIR}.tgz wget http://files.js8call.com/${JS8CALL_VERSION}/${JS8CALL_TGZ} tar xfz ${JS8CALL_TGZ} -#cp ${JS8CALL_DIR}/CMakeLists.txt ./CMakeLists.orig.txt -#sed "s/set (hamlib_STATIC 1)/set (hamlib_STATIC 0)/" < ./CMakeLists.orig.txt > ${JS8CALL_DIR}/CMakeLists.txt -#rm ./CMakeLists.orig.txt +# patch allows us to build against the packaged hamlib patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch rm /js8call-hamlib.patch CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_LEGACY_HAMLIB" cmakebuild ${JS8CALL_DIR} From 4b8ef2977536021d7dc52ed79dd76a8875b5080e Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 23 May 2020 22:55:00 +0200 Subject: [PATCH 345/475] add the fmv-optimized owrx_connector in docker, too, refs #38 --- docker/scripts/install-connectors.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/install-connectors.sh b/docker/scripts/install-connectors.sh index 15d61dc..ffd7a97 100755 --- a/docker/scripts/install-connectors.sh +++ b/docker/scripts/install-connectors.sh @@ -24,7 +24,7 @@ apt-get update apt-get -y install --no-install-recommends $BUILD_PACKAGES git clone https://github.com/jketterl/owrx_connector.git -cmakebuild owrx_connector 3ffbdcccbe39a5675fd539d959e64885e521eae6 +cmakebuild owrx_connector dee422ec6bf59e2ed8dc636e92dafaea5d337723 apt-get -y purge --autoremove $BUILD_PACKAGES apt-get clean From 17f4f671a66fcbe2271c5332baf6abc4abb63732 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 00:41:18 +0200 Subject: [PATCH 346/475] add a changelog entry about docker debian rebuild --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c2046b..81bb8c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ [the js8py library](https://github.com/jketterl/js8py)) - Reorganization of the frontend demodulator code - Improve receiver load time by concatenating javascript assets +- Docker images migrated to Debian slim images; This was necessary to allow the use of function multiversioning in + csdr and owrx_connector to allow the images to run on a wider range of CPUs **0.18.0** - Support for SoapyRemote From e1f83727b75b315c6671ac968229719a50fd5c78 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 00:42:47 +0200 Subject: [PATCH 347/475] update csdr to latest --- docker/scripts/install-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index cee0f14..c6d650a 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -36,7 +36,7 @@ cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d git clone https://github.com/jketterl/csdr.git cd csdr -git checkout 5c8dfdea5fa6e87fa2e9f754be883086fc5a2bfb +git checkout 3ae997c3508b48b9a854c48a628f714eb30a9275 autoreconf -i ./configure make From 29b3f530d2808c30c8592ba48ae3ab000a92ca65 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 02:44:55 +0200 Subject: [PATCH 348/475] update again, latest fixes for aarch64 --- docker/scripts/install-dependencies.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index c6d650a..29588d5 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -36,7 +36,7 @@ cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d git clone https://github.com/jketterl/csdr.git cd csdr -git checkout 3ae997c3508b48b9a854c48a628f714eb30a9275 +git checkout 69c4d74a5b8207b0edf4a36a5a0795fbee39281f autoreconf -i ./configure make From f1d9a4a28c6c730efc80f1458da94d1e508e1873 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 03:04:20 +0200 Subject: [PATCH 349/475] switch to shift_addfast_cc for better performance --- csdr/csdr.py | 4 ++-- owrx/source/resampler.py | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/csdr/csdr.py b/csdr/csdr.py index 9d23978..260da7a 100644 --- a/csdr/csdr.py +++ b/csdr/csdr.py @@ -241,7 +241,7 @@ class dsp(object): if self.fft_compression == "adpcm": chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"] return chain - chain += ["csdr shift_addition_cc --fifo {shift_pipe}"] + chain += ["csdr shift_addfast_cc --fifo {shift_pipe}"] if self.decimation > 1: chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"] chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"] @@ -331,7 +331,7 @@ class dsp(object): return chain elif which == "bpsk31" or which == "bpsk63": return chain + [ - "csdr shift_addition_cc --fifo {secondary_shift_pipe}", + "csdr shift_addfast_cc --fifo {secondary_shift_pipe}", "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}", "csdr simple_agc_cc 0.001 0.5", "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q", diff --git a/owrx/source/resampler.py b/owrx/source/resampler.py index 6afe50c..1f6a4e1 100644 --- a/owrx/source/resampler.py +++ b/owrx/source/resampler.py @@ -1,10 +1,4 @@ from .direct import DirectSource -from . import SdrSource -import subprocess -import threading -import os -import socket -import time import logging @@ -29,7 +23,7 @@ class Resampler(DirectSource): def getCommand(self): return [ "nc -v 127.0.0.1 {nc_port}".format(nc_port=self.sdr.getPort()), - "csdr shift_addition_cc {shift}".format(shift=self.shift), + "csdr shift_addfast_cc {shift}".format(shift=self.shift), "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING".format( decimation=self.decimation, ddc_transition_bw=self.transition_bw ), From a1da59121849a741e817dd46194a1ebd818799bd Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 13:50:28 +0200 Subject: [PATCH 350/475] rtl_connector optimization --- docker/scripts/install-connectors.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/install-connectors.sh b/docker/scripts/install-connectors.sh index ffd7a97..3f5861b 100755 --- a/docker/scripts/install-connectors.sh +++ b/docker/scripts/install-connectors.sh @@ -24,7 +24,7 @@ apt-get update apt-get -y install --no-install-recommends $BUILD_PACKAGES git clone https://github.com/jketterl/owrx_connector.git -cmakebuild owrx_connector dee422ec6bf59e2ed8dc636e92dafaea5d337723 +cmakebuild owrx_connector 4cb8d14fbe387b1569a5b635d7819266ce1dd42b apt-get -y purge --autoremove $BUILD_PACKAGES apt-get clean From 3a8256e3bcd5956200df2a528400e4dc0128cb58 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 14:43:25 +0200 Subject: [PATCH 351/475] update to the sdrplay repository version --- owrx/feature.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/owrx/feature.py b/owrx/feature.py index ae29ced..06eb4f5 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -272,9 +272,9 @@ class FeatureDetector(object): """ The SoapySDR module for sdrplay devices is required for interfacing with SDRPlay devices (RSP1*, RSP2*, RSPDuo) - You can get it [here](https://github.com/pothosware/SoapySDRPlay/wiki). + You can get it [here](https://github.com/SDRplay/SoapySDRPlay). """ - return self._has_soapy_driver("sdrplay") or self._has_soapy_driver("sdrPlay") + return self._has_soapy_driver("sdrplay") def has_soapy_airspy(self): """ From d2be712de86ec8479e7f80a1409d3768d8398c64 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 14:46:17 +0200 Subject: [PATCH 352/475] include sdrplay lib from sdrplay repo --- docker/scripts/install-dependencies-sdrplay.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/scripts/install-dependencies-sdrplay.sh b/docker/scripts/install-dependencies-sdrplay.sh index 8e0a0cd..53cb5ff 100755 --- a/docker/scripts/install-dependencies-sdrplay.sh +++ b/docker/scripts/install-dependencies-sdrplay.sh @@ -48,8 +48,8 @@ cd .. rm -rf sdrplay rm $BINARY -git clone https://github.com/fventuri/SoapySDRPlay.git -cmakebuild SoapySDRPlay 9746de21d5a3778c444cc5e70da2a61c27cb614a +git clone https://github.com/SDRplay/SoapySDRPlay.git +cmakebuild SoapySDRPlay 1c2728a04db5edf8154d02f5cca87e655152d7c1 SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES apt-get clean From 048210d7da746b07b088a908461b95b012ff419a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 14:59:11 +0200 Subject: [PATCH 353/475] update to latest versions from the homepage --- docker/scripts/install-dependencies-sdrplay.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/scripts/install-dependencies-sdrplay.sh b/docker/scripts/install-dependencies-sdrplay.sh index 53cb5ff..151c031 100755 --- a/docker/scripts/install-dependencies-sdrplay.sh +++ b/docker/scripts/install-dependencies-sdrplay.sh @@ -28,13 +28,13 @@ ARCH=$(uname -m) case $ARCH in x86_64) - BINARY=SDRplay_RSP_API-Linux-3.06.1.run + BINARY=SDRplay_RSP_API-Linux-3.07.1.run ;; armv*) - BINARY=SDRplay_RSP_API-ARM32-3.06.1.run + BINARY=SDRplay_RSP_API-ARM32-3.07.2.run ;; aarch64) - BINARY=SDRplay_RSP_API-ARM64-3.06.1.run + BINARY=SDRplay_RSP_API-ARM64-3.07.1.run ;; esac From 6c307d885ff9ca44b4f91eccfe71ac55d926c928 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 16:00:36 +0200 Subject: [PATCH 354/475] integrate s6 service layer for sdrplay --- docker/Dockerfiles/Dockerfile-base | 9 +++++++-- docker/Dockerfiles/Dockerfile-full | 4 +++- docker/Dockerfiles/Dockerfile-sdrplay | 4 +++- docker/{scripts => files/js8call}/js8call-hamlib.patch | 0 .../{scripts => files/sdrplay}/install-lib.aarch64.patch | 0 .../{scripts => files/sdrplay}/install-lib.armv7l.patch | 0 .../{scripts => files/sdrplay}/install-lib.x86_64.patch | 0 docker/files/services/openwebrx/run | 2 ++ docker/files/services/sdrplay/run | 2 ++ 9 files changed, 17 insertions(+), 4 deletions(-) rename docker/{scripts => files/js8call}/js8call-hamlib.patch (100%) rename docker/{scripts => files/sdrplay}/install-lib.aarch64.patch (100%) rename docker/{scripts => files/sdrplay}/install-lib.armv7l.patch (100%) rename docker/{scripts => files/sdrplay}/install-lib.x86_64.patch (100%) create mode 100755 docker/files/services/openwebrx/run create mode 100755 docker/files/services/sdrplay/run diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base index 2ac1cd4..3797cc7 100644 --- a/docker/Dockerfiles/Dockerfile-base +++ b/docker/Dockerfiles/Dockerfile-base @@ -1,6 +1,10 @@ FROM debian:buster-slim -ADD docker/scripts/js8call-hamlib.patch / +ADD https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-amd64.tar.gz /tmp/ +RUN tar xzf /tmp/s6-overlay-amd64.tar.gz -C / +ENTRYPOINT ["/init"] + +ADD docker/files/js8call/js8call-hamlib.patch / ADD docker/scripts/install-dependencies.sh / RUN /install-dependencies.sh && \ rm /install-dependencies.sh @@ -9,5 +13,6 @@ WORKDIR /opt/openwebrx VOLUME /etc/openwebrx -ENTRYPOINT [ "/opt/openwebrx/docker/scripts/run.sh" ] +ADD docker/files/services/openwebrx /etc/services.d/openwebrx + EXPOSE 8073 diff --git a/docker/Dockerfiles/Dockerfile-full b/docker/Dockerfiles/Dockerfile-full index 9571428..fbb3bd7 100644 --- a/docker/Dockerfiles/Dockerfile-full +++ b/docker/Dockerfiles/Dockerfile-full @@ -2,7 +2,7 @@ ARG ARCHTAG FROM openwebrx-base:$ARCHTAG ADD docker/scripts/install-dependencies-*.sh / -ADD docker/scripts/install-lib.*.patch / +ADD docker/files/sdrplay/install-lib.*.patch / RUN /install-dependencies-rtlsdr.sh &&\ /install-dependencies-hackrf.sh &&\ @@ -21,4 +21,6 @@ ADD docker/scripts/install-connectors.sh / RUN /install-connectors.sh &&\ rm /install-connectors.sh +ADD docker/files/services/sdrplay /etc/services.d/sdrplay + ADD . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-sdrplay b/docker/Dockerfiles/Dockerfile-sdrplay index eb4cf2c..ec649f2 100644 --- a/docker/Dockerfiles/Dockerfile-sdrplay +++ b/docker/Dockerfiles/Dockerfile-sdrplay @@ -2,9 +2,11 @@ ARG ARCHTAG FROM openwebrx-soapysdr-base:$ARCHTAG ADD docker/scripts/install-dependencies-sdrplay.sh / -ADD docker/scripts/install-lib.*.patch / +ADD docker/files/sdrplay/install-lib.*.patch / RUN /install-dependencies-sdrplay.sh &&\ rm /install-dependencies-sdrplay.sh &&\ rm /install-lib.*.patch +ADD docker/files/services/sdrplay /etc/services.d/sdrplay + ADD . /opt/openwebrx diff --git a/docker/scripts/js8call-hamlib.patch b/docker/files/js8call/js8call-hamlib.patch similarity index 100% rename from docker/scripts/js8call-hamlib.patch rename to docker/files/js8call/js8call-hamlib.patch diff --git a/docker/scripts/install-lib.aarch64.patch b/docker/files/sdrplay/install-lib.aarch64.patch similarity index 100% rename from docker/scripts/install-lib.aarch64.patch rename to docker/files/sdrplay/install-lib.aarch64.patch diff --git a/docker/scripts/install-lib.armv7l.patch b/docker/files/sdrplay/install-lib.armv7l.patch similarity index 100% rename from docker/scripts/install-lib.armv7l.patch rename to docker/files/sdrplay/install-lib.armv7l.patch diff --git a/docker/scripts/install-lib.x86_64.patch b/docker/files/sdrplay/install-lib.x86_64.patch similarity index 100% rename from docker/scripts/install-lib.x86_64.patch rename to docker/files/sdrplay/install-lib.x86_64.patch diff --git a/docker/files/services/openwebrx/run b/docker/files/services/openwebrx/run new file mode 100755 index 0000000..2c2b7fd --- /dev/null +++ b/docker/files/services/openwebrx/run @@ -0,0 +1,2 @@ +#!/usr/bin/execlineb -P +/opt/openwebrx/docker/scripts/run.sh \ No newline at end of file diff --git a/docker/files/services/sdrplay/run b/docker/files/services/sdrplay/run new file mode 100755 index 0000000..0f31c4c --- /dev/null +++ b/docker/files/services/sdrplay/run @@ -0,0 +1,2 @@ +#!/usr/bin/execlineb -P +/usr/local/bin/sdrplay_apiService \ No newline at end of file From a982c86794fa8e503cc819fd983a0c93dce402ec Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 17:28:48 +0200 Subject: [PATCH 355/475] update sdrplay patches; fix sdrplay service --- docker/Dockerfiles/Dockerfile-base | 2 +- .../files/sdrplay/install-lib.aarch64.patch | 30 +++-------- docker/files/sdrplay/install-lib.armv7l.patch | 51 +++++++++---------- docker/files/sdrplay/install-lib.x86_64.patch | 21 +++++++- docker/files/services/openwebrx/run | 2 - .../scripts/install-dependencies-sdrplay.sh | 4 +- 6 files changed, 53 insertions(+), 57 deletions(-) delete mode 100755 docker/files/services/openwebrx/run diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base index 3797cc7..654bf02 100644 --- a/docker/Dockerfiles/Dockerfile-base +++ b/docker/Dockerfiles/Dockerfile-base @@ -13,6 +13,6 @@ WORKDIR /opt/openwebrx VOLUME /etc/openwebrx -ADD docker/files/services/openwebrx /etc/services.d/openwebrx +CMD /opt/openwebrx/docker/scripts/run.sh EXPOSE 8073 diff --git a/docker/files/sdrplay/install-lib.aarch64.patch b/docker/files/sdrplay/install-lib.aarch64.patch index 6f1323d..1f3dc57 100644 --- a/docker/files/sdrplay/install-lib.aarch64.patch +++ b/docker/files/sdrplay/install-lib.aarch64.patch @@ -1,11 +1,12 @@ ---- sdrplay/install_lib.sh -+++ sdrplay/install_lib_patched.sh -@@ -4,20 +4,7 @@ +diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh +--- sdrplay-orig/install_lib.sh 2020-05-24 14:30:06.022483867 +0000 ++++ sdrplay/install_lib.sh 2020-05-24 14:30:49.093435726 +0000 +@@ -4,19 +4,6 @@ export MAJVERS="3" echo "Installing SDRplay RSP API library ${VERS}..." -read -p "Press RETURN to view the license agreement" ret - +- -more sdrplay_license.txt - -while true; do @@ -17,25 +18,6 @@ - * ) echo "Please answer y or n";; - esac -done -- + export ARCH=`uname -m` - echo "Architecture: ${ARCH}" -@@ -46,17 +33,6 @@ - echo "ERROR: udev rules directory not found, add udev support and run the" - echo "installer again. udev support can be added by running..." - echo "sudo apt-get install libudev-dev" -- echo " " -- exit 1 --fi -- --if /sbin/ldconfig -p | /bin/fgrep -q libusb-1.0; then -- echo "Libusb found, continuing..." --else -- echo " " -- echo "ERROR: Libusb cannot be found. Please install libusb and then run" -- echo "the installer again. Libusb can be installed by running..." -- echo "sudo apt-get install libusb-1.0.0-dev" - echo " " - exit 1 - fi diff --git a/docker/files/sdrplay/install-lib.armv7l.patch b/docker/files/sdrplay/install-lib.armv7l.patch index 775ef89..22a78f6 100644 --- a/docker/files/sdrplay/install-lib.armv7l.patch +++ b/docker/files/sdrplay/install-lib.armv7l.patch @@ -1,11 +1,12 @@ ---- sdrplay/install_lib.sh -+++ sdrplay/install_lib_patched.sh -@@ -4,20 +4,7 @@ - export MAJVERS="3" +diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh +--- sdrplay-orig/install_lib.sh 2020-05-24 14:13:04.561271707 +0000 ++++ sdrplay/install_lib.sh 2020-05-24 14:16:20.068329040 +0000 +@@ -4,19 +4,6 @@ + MAJVERS="3" echo "Installing SDRplay RSP API library ${VERS}..." -read -p "Press RETURN to view the license agreement" ret - +- -more sdrplay_license.txt - -while true; do @@ -17,25 +18,23 @@ - * ) echo "Please answer y or n";; - esac -done -- - export ARCH=`uname -m` - echo "Architecture: ${ARCH}" -@@ -44,17 +31,6 @@ - echo "ERROR: udev rules directory not found, add udev support and run the" - echo "installer again. udev support can be added by running..." - echo "sudo apt-get install libudev-dev" -- echo " " -- exit 1 --fi -- --if /sbin/ldconfig -p | /bin/fgrep -q libusb-1.0; then -- echo "Libusb found, continuing..." --else -- echo " " -- echo "ERROR: Libusb cannot be found. Please install libusb and then run" -- echo "the installer again. Libusb can be installed by running..." -- echo "sudo apt-get install libusb-1.0.0-dev" - echo " " - exit 1 - fi + ARCH=`uname -m` + +@@ -141,16 +128,6 @@ + echo "SDRplay API ${VERS} Installation Finished" + echo " " + +-while true; do +- echo "Would you like to add SDRplay USB IDs to the local database for easier +-" +- read -p "identification in applications such as lsusb? [y/n] " yn +- case $yn in +- [Yy]* ) break;; +- [Nn]* ) exit;; +- * ) echo "Please answer y or n";; +- esac +-done + sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/. + sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh + sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/. diff --git a/docker/files/sdrplay/install-lib.x86_64.patch b/docker/files/sdrplay/install-lib.x86_64.patch index 0666bf4..d66023b 100644 --- a/docker/files/sdrplay/install-lib.x86_64.patch +++ b/docker/files/sdrplay/install-lib.x86_64.patch @@ -1,5 +1,6 @@ ---- sdrplay/install_lib.sh -+++ sdrplay/install_lib_patched.sh +diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh +--- sdrplay-orig/install_lib.sh 2020-05-24 13:56:56.622000041 +0000 ++++ sdrplay/install_lib.sh 2020-05-24 13:58:51.837801559 +0000 @@ -4,19 +4,6 @@ MAJVERS="3" @@ -20,3 +21,19 @@ ARCH=`uname -m` OSDIST="Unknown" +@@ -157,15 +144,6 @@ + echo " " + echo "SDRplay API ${VERS} Installation Finished" + echo " " +-while true; do +- echo "Would you like to add SDRplay USB IDs to the local database for easier" +- read -p "identification in applications such as lsusb? [y/n] " yn +- case $yn in +- [Yy]* ) break;; +- [Nn]* ) exit;; +- * ) echo "Please answer y or n";; +- esac +-done + sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/. + sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh + sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/. diff --git a/docker/files/services/openwebrx/run b/docker/files/services/openwebrx/run deleted file mode 100755 index 2c2b7fd..0000000 --- a/docker/files/services/openwebrx/run +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/execlineb -P -/opt/openwebrx/docker/scripts/run.sh \ No newline at end of file diff --git a/docker/scripts/install-dependencies-sdrplay.sh b/docker/scripts/install-dependencies-sdrplay.sh index 151c031..d18fcc0 100755 --- a/docker/scripts/install-dependencies-sdrplay.sh +++ b/docker/scripts/install-dependencies-sdrplay.sh @@ -18,8 +18,8 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="libusb-1.0.0 libudev1" -BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev" +STATIC_PACKAGES="libusb-1.0.0 udev" +BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev" apt-get update apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES From d64f08490abf44f9ab57f8b823febf33a253b55a Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 18:00:14 +0200 Subject: [PATCH 356/475] use the old syntax --- docker/Dockerfiles/Dockerfile-base | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base index 654bf02..66642b0 100644 --- a/docker/Dockerfiles/Dockerfile-base +++ b/docker/Dockerfiles/Dockerfile-base @@ -13,6 +13,6 @@ WORKDIR /opt/openwebrx VOLUME /etc/openwebrx -CMD /opt/openwebrx/docker/scripts/run.sh +CMD [ "/opt/openwebrx/docker/scripts/run.sh" ] EXPOSE 8073 From d9db693aecf4e9168773bc30c500c09cfb084405 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 18:02:45 +0200 Subject: [PATCH 357/475] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81bb8c7..6bf8754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Improve receiver load time by concatenating javascript assets - Docker images migrated to Debian slim images; This was necessary to allow the use of function multiversioning in csdr and owrx_connector to allow the images to run on a wider range of CPUs +- Docker containers have been updated to include the SDRplay driver version 3 **0.18.0** - Support for SoapyRemote From 305adc94facf97a0339b30d707e8b1f85d152ff2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 24 May 2020 21:45:08 +0200 Subject: [PATCH 358/475] install s6 overlay for the right platform --- docker/Dockerfiles/Dockerfile-base | 5 +++-- docker/scripts/install-s6.sh | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) create mode 100755 docker/scripts/install-s6.sh diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base index 66642b0..d3d6f21 100644 --- a/docker/Dockerfiles/Dockerfile-base +++ b/docker/Dockerfiles/Dockerfile-base @@ -1,7 +1,8 @@ FROM debian:buster-slim -ADD https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-amd64.tar.gz /tmp/ -RUN tar xzf /tmp/s6-overlay-amd64.tar.gz -C / +ADD docker/scripts/install-s6.sh / +RUN /install-s6.sh && \ + rm /install-s6.sh ENTRYPOINT ["/init"] ADD docker/files/js8call/js8call-hamlib.patch / diff --git a/docker/scripts/install-s6.sh b/docker/scripts/install-s6.sh new file mode 100755 index 0000000..a3551e2 --- /dev/null +++ b/docker/scripts/install-s6.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -eux pipefail + +BUILD_PACKAGES="wget ca-certificates" + +apt-get update +apt-get -y install --no-install-recommends $BUILD_PACKAGES + +case `uname -m` in + arm*) + PLATFORM=armhf + ;; + aarch64*) + PLATFORM=aarch64 + ;; + x86_64*) + PLATFORM=amd64 + ;; +esac + +pushd /tmp +wget https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${PLATFORM}.tar.gz +tar xzf s6-overlay-${PLATFORM}.tar.gz -C / +rm s6-overlay-${PLATFORM}.tar.gz +popd + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* From d24abd436e9b29e6558e99eb65ac564fce2e2663 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 25 May 2020 20:10:03 +0200 Subject: [PATCH 359/475] install s6 overlay during normal dependency setup --- docker/Dockerfiles/Dockerfile-base | 7 ++----- docker/scripts/install-dependencies.sh | 18 ++++++++++++++++ docker/scripts/install-s6.sh | 29 -------------------------- 3 files changed, 20 insertions(+), 34 deletions(-) delete mode 100755 docker/scripts/install-s6.sh diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base index d3d6f21..c86d464 100644 --- a/docker/Dockerfiles/Dockerfile-base +++ b/docker/Dockerfiles/Dockerfile-base @@ -1,15 +1,12 @@ FROM debian:buster-slim -ADD docker/scripts/install-s6.sh / -RUN /install-s6.sh && \ - rm /install-s6.sh -ENTRYPOINT ["/init"] - ADD docker/files/js8call/js8call-hamlib.patch / ADD docker/scripts/install-dependencies.sh / RUN /install-dependencies.sh && \ rm /install-dependencies.sh +ENTRYPOINT ["/init"] + WORKDIR /opt/openwebrx VOLUME /etc/openwebrx diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 29588d5..a5684d8 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -24,6 +24,24 @@ BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapa apt-get update apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES +case `uname -m` in + arm*) + PLATFORM=armhf + ;; + aarch64*) + PLATFORM=aarch64 + ;; + x86_64*) + PLATFORM=amd64 + ;; +esac + +pushd /tmp +wget https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${PLATFORM}.tar.gz +tar xzf s6-overlay-${PLATFORM}.tar.gz -C / +rm s6-overlay-${PLATFORM}.tar.gz +popd + git clone https://github.com/jketterl/js8py.git pushd js8py git checkout 888e62be375316882ad2b2ac8e396c3bf857b6fc diff --git a/docker/scripts/install-s6.sh b/docker/scripts/install-s6.sh deleted file mode 100755 index a3551e2..0000000 --- a/docker/scripts/install-s6.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -set -eux pipefail - -BUILD_PACKAGES="wget ca-certificates" - -apt-get update -apt-get -y install --no-install-recommends $BUILD_PACKAGES - -case `uname -m` in - arm*) - PLATFORM=armhf - ;; - aarch64*) - PLATFORM=aarch64 - ;; - x86_64*) - PLATFORM=amd64 - ;; -esac - -pushd /tmp -wget https://github.com/just-containers/s6-overlay/releases/download/v1.21.8.0/s6-overlay-${PLATFORM}.tar.gz -tar xzf s6-overlay-${PLATFORM}.tar.gz -C / -rm s6-overlay-${PLATFORM}.tar.gz -popd - -apt-get -y purge --autoremove $BUILD_PACKAGES -apt-get clean -rm -rf /var/lib/apt/lists/* From d2a4f2bc464f81c3109e5d6ee17ada6063ea81d8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 25 May 2020 20:30:53 +0200 Subject: [PATCH 360/475] patch wsjt-x to use packaged hamlib, too --- docker/Dockerfiles/Dockerfile-base | 4 ++- docker/files/wsjtx/wsjtx-hamlib.patch | 43 +++++++++++++++++++++++ docker/files/wsjtx/wsjtx.patch | 49 +++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 docker/files/wsjtx/wsjtx-hamlib.patch create mode 100644 docker/files/wsjtx/wsjtx.patch diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base index c86d464..c044fd7 100644 --- a/docker/Dockerfiles/Dockerfile-base +++ b/docker/Dockerfiles/Dockerfile-base @@ -1,9 +1,11 @@ FROM debian:buster-slim ADD docker/files/js8call/js8call-hamlib.patch / +ADD docker/files/wsjtx/*.patch / ADD docker/scripts/install-dependencies.sh / RUN /install-dependencies.sh && \ - rm /install-dependencies.sh + rm /install-dependencies.sh && \ + rm /*.patch ENTRYPOINT ["/init"] diff --git a/docker/files/wsjtx/wsjtx-hamlib.patch b/docker/files/wsjtx/wsjtx-hamlib.patch new file mode 100644 index 0000000..84f7861 --- /dev/null +++ b/docker/files/wsjtx/wsjtx-hamlib.patch @@ -0,0 +1,43 @@ +--- CMakeLists.txt 2020-05-25 19:26:41.423517236 +0200 ++++ CMakeLists.txt 2020-05-25 19:11:36.116236231 +0200 +@@ -79,24 +79,6 @@ + + include (ExternalProject) + +- +-# +-# build and install hamlib locally so it can be referenced by the +-# WSJT-X build +-# +-ExternalProject_Add (hamlib +- GIT_REPOSITORY ${hamlib_repo} +- GIT_TAG ${hamlib_TAG} +- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/hamlib.tgz +- URL_HASH MD5=${hamlib_md5sum} +- UPDATE_COMMAND ./bootstrap +- PATCH_COMMAND ${PATCH_EXECUTABLE} -p1 -N < ${CMAKE_CURRENT_SOURCE_DIR}/hamlib.patch +- CONFIGURE_COMMAND /configure --prefix= --disable-shared --enable-static --without-cxx-binding ${EXTRA_FLAGS} # LIBUSB_LIBS=${USB_LIBRARY} +- BUILD_COMMAND $(MAKE) all V=1 # $(MAKE) is ExternalProject_Add() magic to do recursive make +- INSTALL_COMMAND $(MAKE) install-strip V=1 DESTDIR="" +- STEP_TARGETS update install +- ) +- + # + # custom target to make a hamlib source tarball + # +@@ -128,7 +110,6 @@ + # build and optionally install WSJT-X using the hamlib package built + # above + # +-ExternalProject_Get_Property (hamlib INSTALL_DIR) + ExternalProject_Add (wsjtx + GIT_REPOSITORY ${wsjtx_repo} + GIT_TAG ${WSJTX_TAG} +@@ -152,7 +133,6 @@ + DEPENDEES build + ) + +-set_target_properties (hamlib PROPERTIES EXCLUDE_FROM_ALL 1) + set_target_properties (wsjtx PROPERTIES EXCLUDE_FROM_ALL 1) + + add_dependencies (wsjtx-configure hamlib-install) diff --git a/docker/files/wsjtx/wsjtx.patch b/docker/files/wsjtx/wsjtx.patch new file mode 100644 index 0000000..bde05e4 --- /dev/null +++ b/docker/files/wsjtx/wsjtx.patch @@ -0,0 +1,49 @@ +diff -ur js8call-2.1.1-orig/CMake/Modules/Findhamlib.cmake js8call-2.1.1/CMake/Modules/Findhamlib.cmake +--- js8call-2.1.1-orig/CMake/Modules/Findhamlib.cmake 2020-05-23 15:38:20.730349612 +0000 ++++ js8call-2.1.1/CMake/Modules/Findhamlib.cmake 2020-05-23 15:39:28.829772207 +0000 +@@ -85,4 +85,4 @@ + # Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to + # TRUE if all listed variables are TRUE + include (FindPackageHandleStandardArgs) +-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS) ++find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES) +diff -ur js8call-2.1.1-orig/CMakeLists.txt js8call-2.1.1/CMakeLists.txt +--- js8call-2.1.1-orig/CMakeLists.txt 2020-05-23 15:38:20.730349612 +0000 ++++ js8call-2.1.1/CMakeLists.txt 2020-05-23 15:52:46.103389553 +0000 +@@ -869,7 +869,7 @@ + # + # libhamlib setup + # +-set (hamlib_STATIC 1) ++set (hamlib_STATIC 0) + find_package (hamlib 3 REQUIRED) + find_program (RIGCTL_EXE rigctl) + find_program (RIGCTLD_EXE rigctld) +@@ -1471,27 +1404,6 @@ + ) + endif(WSJT_BUILD_UTILS) + +-install (PROGRAMS +- ${RIGCTL_EXE} +- DESTINATION ${CMAKE_INSTALL_BINDIR} +- #COMPONENT runtime +- RENAME rigctl-wsjtx${CMAKE_EXECUTABLE_SUFFIX} +- ) +- +-install (PROGRAMS +- ${RIGCTLD_EXE} +- DESTINATION ${CMAKE_INSTALL_BINDIR} +- #COMPONENT runtime +- RENAME rigctld-wsjtx${CMAKE_EXECUTABLE_SUFFIX} +- ) +- +-install (PROGRAMS +- ${RIGCTLCOM_EXE} +- DESTINATION ${CMAKE_INSTALL_BINDIR} +- #COMPONENT runtime +- RENAME rigctlcom-wsjtx${CMAKE_EXECUTABLE_SUFFIX} +- ) +- + install (FILES + README + COPYING From 27d6802dfc9636cf079a4baae9de8e55710df9d5 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 25 May 2020 20:31:42 +0200 Subject: [PATCH 361/475] include wsjt-x patches --- docker/scripts/install-dependencies.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index a5684d8..a801ad6 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -86,6 +86,8 @@ WSJT_DIR=wsjtx-2.1.2 WSJT_TGZ=${WSJT_DIR}.tgz wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ} tar xfz ${WSJT_TGZ} +patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch +mv /wsjtx.patch ${WSJT_DIR} cmakebuild ${WSJT_DIR} rm ${WSJT_TGZ} From 564c1e26b6c9d46739c9f922bba62d934fd52010 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 25 May 2020 20:38:42 +0200 Subject: [PATCH 362/475] let's try auto-apt-proxy to cut down build times --- docker/scripts/install-dependencies.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index a801ad6..206a5e0 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -22,6 +22,7 @@ STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libs BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev autoconf automake libtool texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev pkg-config libudev-dev libhamlib-dev patch xsltproc" apt-get update +apt-get -y install auto-apt-proxy apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES case `uname -m` in From e0648d63add24b9bcfc7fcea79c506fc6161c963 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 28 May 2020 00:45:27 +0200 Subject: [PATCH 363/475] reduce image size by excluding wsjt-x and js8call frontend binaries --- docker/files/js8call/js8call-hamlib.patch | 127 +++++++++++++++++++-- docker/files/wsjtx/wsjtx.patch | 129 ++++++++++++++++++++-- docker/scripts/install-dependencies.sh | 2 +- 3 files changed, 236 insertions(+), 22 deletions(-) diff --git a/docker/files/js8call/js8call-hamlib.patch b/docker/files/js8call/js8call-hamlib.patch index 68c1e70..2bc6141 100644 --- a/docker/files/js8call/js8call-hamlib.patch +++ b/docker/files/js8call/js8call-hamlib.patch @@ -1,15 +1,15 @@ -diff -ur js8call-2.1.1-orig/CMake/Modules/Findhamlib.cmake js8call-2.1.1/CMake/Modules/Findhamlib.cmake ---- js8call-2.1.1-orig/CMake/Modules/Findhamlib.cmake 2020-05-23 15:38:20.730349612 +0000 -+++ js8call-2.1.1/CMake/Modules/Findhamlib.cmake 2020-05-23 15:39:28.829772207 +0000 +diff -ur js8call-orig/CMake/Modules/Findhamlib.cmake js8call/CMake/Modules/Findhamlib.cmake +--- js8call-orig/CMake/Modules/Findhamlib.cmake 2020-05-28 00:10:13.386429366 +0200 ++++ js8call/CMake/Modules/Findhamlib.cmake 2020-05-28 00:10:34.339623106 +0200 @@ -78,4 +78,4 @@ # Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to # TRUE if all listed variables are TRUE include (FindPackageHandleStandardArgs) -find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS) +find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES) -diff -ur js8call-2.1.1-orig/CMakeLists.txt js8call-2.1.1/CMakeLists.txt ---- js8call-2.1.1-orig/CMakeLists.txt 2020-05-23 15:38:20.730349612 +0000 -+++ js8call-2.1.1/CMakeLists.txt 2020-05-23 15:52:46.103389553 +0000 +diff -ur js8call-orig/CMakeLists.txt js8call/CMakeLists.txt +--- js8call-orig/CMakeLists.txt 2020-05-28 00:10:13.393095987 +0200 ++++ js8call/CMakeLists.txt 2020-05-28 00:12:09.925653037 +0200 @@ -683,7 +683,7 @@ # # libhamlib setup @@ -19,7 +19,72 @@ diff -ur js8call-2.1.1-orig/CMakeLists.txt js8call-2.1.1/CMakeLists.txt find_package (hamlib 3 REQUIRED) find_program (RIGCTL_EXE rigctl) find_program (RIGCTLD_EXE rigctld) -@@ -1106,20 +1106,6 @@ +@@ -1033,55 +1033,6 @@ + target_link_libraries (js8 wsjt_fort wsjt_cxx Qt5::Core) + endif (${OPENMP_FOUND} OR APPLE) + +-# build the main application +-add_executable (js8call MACOSX_BUNDLE +- ${sqlite3_CSRCS} +- ${wsjtx_CXXSRCS} +- ${wsjtx_GENUISRCS} +- wsjtx.rc +- ${WSJTX_ICON_FILE} +- ${wsjtx_RESOURCES_RCC} +- ) +- +-if (WSJT_CREATE_WINMAIN) +- set_target_properties (js8call PROPERTIES WIN32_EXECUTABLE ON) +-endif (WSJT_CREATE_WINMAIN) +- +-set_target_properties (js8call PROPERTIES +- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in" +- MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}" +- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}" +- MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION} +- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}" +- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}" +- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}" +- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}" +- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}" +- MACOSX_BUNDLE_GUI_IDENTIFIER "org.kn4crd.js8call" +- ) +- +-target_include_directories (js8call PRIVATE ${FFTW3_INCLUDE_DIRS}) +-if (APPLE) +- target_link_libraries (js8call wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES}) +-else () +- target_link_libraries (js8call wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES}) +- if (OpenMP_C_FLAGS) +- set_target_properties (js8call PROPERTIES +- COMPILE_FLAGS "${OpenMP_C_FLAGS}" +- LINK_FLAGS "${OpenMP_C_FLAGS}" +- ) +- endif () +- set_target_properties (js8call PROPERTIES +- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp +- ) +- if (WIN32) +- set_target_properties (js8call PROPERTIES +- LINK_FLAGS -Wl,--stack,16777216 +- ) +- endif () +-endif () +-qt5_use_modules (js8call SerialPort) # not sure why the interface link library syntax above doesn't work +- + # if (UNIX) + # if (NOT WSJT_SKIP_MANPAGES) + # add_subdirectory (manpages) +@@ -1097,38 +1048,10 @@ + # + # installation + # +-install (TARGETS js8call +- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime +- BUNDLE DESTINATION . COMPONENT runtime +- ) +- + install (TARGETS js8 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime ) @@ -36,8 +101,50 @@ diff -ur js8call-2.1.1-orig/CMakeLists.txt js8call-2.1.1/CMakeLists.txt - #COMPONENT runtime - RENAME rigctld-local${CMAKE_EXECUTABLE_SUFFIX} - ) +- +-install (FILES +- README +- COPYING +- INSTALL +- INSTALL-WSJTX +- DESTINATION ${CMAKE_INSTALL_DOCDIR} +- #COMPONENT runtime +- ) - install (FILES - README - COPYING -Only in js8call-2.1.1/: hamlib.patch + contrib/Ephemeris/JPLEPH + DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME} +@@ -1182,32 +1105,6 @@ + "${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h" + ) + +- +-if (NOT WIN32 AND NOT APPLE) +- # install a desktop file so js8call appears in the application start +- # menu with an icon +- install ( +- FILES js8call.desktop +- DESTINATION /usr/share/applications +- #COMPONENT runtime +- ) +- install ( +- FILES icons/Unix/js8call_icon.png +- DESTINATION /usr/share/pixmaps +- #COMPONENT runtime +- ) +- +- IF("${CMAKE_INSTALL_PREFIX}" STREQUAL "/opt/js8call") +- execute_process(COMMAND ln -s /opt/js8call/bin/js8call ljs8call) +- +- install(FILES +- ${CMAKE_BINARY_DIR}/ljs8call DESTINATION /usr/bin/ RENAME js8call +- #COMPONENT runtime +- ) +- endif() +-endif (NOT WIN32 AND NOT APPLE) +- +- + # + # bundle fixup only done in Release or MinSizeRel configurations + # +Only in js8call/: .idea diff --git a/docker/files/wsjtx/wsjtx.patch b/docker/files/wsjtx/wsjtx.patch index bde05e4..b7854bb 100644 --- a/docker/files/wsjtx/wsjtx.patch +++ b/docker/files/wsjtx/wsjtx.patch @@ -1,15 +1,15 @@ -diff -ur js8call-2.1.1-orig/CMake/Modules/Findhamlib.cmake js8call-2.1.1/CMake/Modules/Findhamlib.cmake ---- js8call-2.1.1-orig/CMake/Modules/Findhamlib.cmake 2020-05-23 15:38:20.730349612 +0000 -+++ js8call-2.1.1/CMake/Modules/Findhamlib.cmake 2020-05-23 15:39:28.829772207 +0000 +diff -ur wsjtx-orig/CMake/Modules/Findhamlib.cmake wsjtx/CMake/Modules/Findhamlib.cmake +--- wsjtx-orig/CMake/Modules/Findhamlib.cmake 2020-05-27 22:41:57.774855748 +0200 ++++ wsjtx/CMake/Modules/Findhamlib.cmake 2020-05-27 22:42:35.267939882 +0200 @@ -85,4 +85,4 @@ # Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to # TRUE if all listed variables are TRUE include (FindPackageHandleStandardArgs) -find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS) +find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES) -diff -ur js8call-2.1.1-orig/CMakeLists.txt js8call-2.1.1/CMakeLists.txt ---- js8call-2.1.1-orig/CMakeLists.txt 2020-05-23 15:38:20.730349612 +0000 -+++ js8call-2.1.1/CMakeLists.txt 2020-05-23 15:52:46.103389553 +0000 +diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt +--- wsjtx-orig/CMakeLists.txt 2020-05-27 22:41:57.774855748 +0200 ++++ wsjtx/CMakeLists.txt 2020-05-27 22:58:18.905001618 +0200 @@ -869,7 +869,7 @@ # # libhamlib setup @@ -19,10 +19,104 @@ diff -ur js8call-2.1.1-orig/CMakeLists.txt js8call-2.1.1/CMakeLists.txt find_package (hamlib 3 REQUIRED) find_program (RIGCTL_EXE rigctl) find_program (RIGCTLD_EXE rigctld) -@@ -1471,27 +1404,6 @@ - ) +@@ -1326,54 +1326,10 @@ + endif(WSJT_BUILD_UTILS) - + +-# build the main application +-add_executable (wsjtx MACOSX_BUNDLE +- ${wsjtx_CXXSRCS} +- ${wsjtx_GENUISRCS} +- wsjtx.rc +- ${WSJTX_ICON_FILE} +- ${wsjtx_RESOURCES_RCC} +- ) +- + if (WSJT_CREATE_WINMAIN) + set_target_properties (wsjtx PROPERTIES WIN32_EXECUTABLE ON) + endif (WSJT_CREATE_WINMAIN) + +-set_target_properties (wsjtx PROPERTIES +- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in" +- MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}" +- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}" +- MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION} +- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}" +- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}" +- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}" +- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}" +- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}" +- MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx" +- ) +- +-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS}) +-if (APPLE) +- target_link_libraries (wsjtx wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES}) +-else () +- target_link_libraries (wsjtx wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES}) +- if (OpenMP_C_FLAGS) +- set_target_properties (wsjtx PROPERTIES +- COMPILE_FLAGS "${OpenMP_C_FLAGS}" +- LINK_FLAGS "${OpenMP_C_FLAGS}" +- ) +- endif () +- set_target_properties (wsjtx PROPERTIES +- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp +- ) +- if (WIN32) +- set_target_properties (wsjtx PROPERTIES +- LINK_FLAGS -Wl,--stack,16777216 +- ) +- endif () +-endif () +-qt5_use_modules (wsjtx SerialPort) # not sure why the interface link library syntax above doesn't work +- + # make a library for WSJT-X UDP servers + # add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS}) + add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS}) +@@ -1417,24 +1373,9 @@ + set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON) + endif (WSJT_CREATE_WINMAIN) + +-if (UNIX) +- if (NOT WSJT_SKIP_MANPAGES) +- add_subdirectory (manpages) +- add_dependencies (wsjtx manpages) +- endif (NOT WSJT_SKIP_MANPAGES) +- if (NOT APPLE) +- add_subdirectory (debian) +- add_dependencies (wsjtx debian) +- endif (NOT APPLE) +-endif (UNIX) +- + # + # installation + # +-install (TARGETS wsjtx +- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime +- BUNDLE DESTINATION . COMPONENT runtime +- ) + + # install (TARGETS wsjtx_udp EXPORT udp + # RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +@@ -1453,12 +1394,7 @@ + # DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx + # ) + +-install (TARGETS udp_daemon message_aggregator +- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime +- BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime +- ) +- +-install (TARGETS jt9 wsprd fmtave fcal fmeasure ++install (TARGETS jt9 wsprd + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime + BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime + ) +@@ -1471,39 +1407,6 @@ + ) + endif(WSJT_BUILD_UTILS) + -install (PROGRAMS - ${RIGCTL_EXE} - DESTINATION ${CMAKE_INSTALL_BINDIR} @@ -43,7 +137,20 @@ diff -ur js8call-2.1.1-orig/CMakeLists.txt js8call-2.1.1/CMakeLists.txt - #COMPONENT runtime - RENAME rigctlcom-wsjtx${CMAKE_EXECUTABLE_SUFFIX} - ) +- +-install (FILES +- README +- COPYING +- AUTHORS +- THANKS +- NEWS +- INSTALL +- BUGS +- DESTINATION ${CMAKE_INSTALL_DOCDIR} +- #COMPONENT runtime +- ) - install (FILES - README - COPYING + contrib/Ephemeris/JPLEPH + DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME} +Only in wsjtx: .idea diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index 206a5e0..f977430 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -18,7 +18,7 @@ function cmakebuild() { cd /tmp -STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5serialport5 libqt5multimedia5-plugins libqt5widgets5 libqt5sql5-sqlite libqt5core5a libqt5multimedia5 libqt5network5 libqt5printsupport5 libqt5serialport5 libqt5sql5 libqt5widgets5 libreadline7 libgfortran4 libgomp1 libasound2 libudev1 libhamlib2 ca-certificates" +STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline7 libgfortran4 libgomp1 libasound2 libudev1 ca-certificates" BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev autoconf automake libtool texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev pkg-config libudev-dev libhamlib-dev patch xsltproc" apt-get update From e90973bcd45b15c44b513de3f3d01d5a36fe9a14 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 30 May 2020 22:58:31 +0200 Subject: [PATCH 364/475] switch hackrf to soapy --- CHANGELOG.md | 3 ++- owrx/feature.py | 30 +++++++++--------------------- owrx/source/hackrf.py | 23 ++++------------------- 3 files changed, 15 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf8754..5f77877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,8 @@ - Improve receiver load time by concatenating javascript assets - Docker images migrated to Debian slim images; This was necessary to allow the use of function multiversioning in csdr and owrx_connector to allow the images to run on a wider range of CPUs -- Docker containers have been updated to include the SDRplay driver version 3 +- Docker containers have been updated to include the SDRplay driver version 3 +- HackRF support is now based on SoapyHackRF **0.18.0** - Support for SoapyRemote diff --git a/owrx/feature.py b/owrx/feature.py index 06eb4f5..9b31001 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -24,7 +24,7 @@ class FeatureDetector(object): "rtl_sdr": ["rtl_connector"], "rtl_sdr_soapy": ["soapy_connector", "soapy_rtl_sdr"], "sdrplay": ["soapy_connector", "soapy_sdrplay"], - "hackrf": ["hackrf_transfer"], + "hackrf": ["soapy_connector", "soapy_hackrf"], "perseussdr": ["perseustest"], "airspy": ["soapy_connector", "soapy_airspy"], "airspyhf": ["soapy_connector", "soapy_airspyhf"], @@ -127,26 +127,6 @@ class FeatureDetector(object): """ return self.command_is_runnable("nc --help") - def has_hackrf_transfer(self): - """ - To use a HackRF, compile the HackRF host tools from its "stdout" branch: - ``` - git clone https://github.com/mossmann/hackrf/ - cd hackrf - git fetch - git checkout origin/stdout - cd host - mkdir build - cd build - cmake .. -DINSTALL_UDEV_RULES=ON - make - sudo make install - ``` - """ - # TODO i don't have a hackrf, so somebody doublecheck this. - # TODO also check if it has the stdout feature - return self.command_is_runnable("hackrf_transfer --help") - def has_perseustest(self): """ To use a Microtelecom Perseus HF receiver, compile and @@ -333,6 +313,14 @@ class FeatureDetector(object): """ return self._has_soapy_driver("redpitaya") + def has_soapy_hackrf(self): + """ + The SoapyHackRF allows HackRF to be used with SoapySDR. + + You can get it [here](https://github.com/pothosware/SoapyHackRF/wiki). + """ + return self._has_soapy_driver("hackrf") + def has_dsd(self): """ The digital voice modes NXDN and D-Star can be decoded by the dsd project. Please note that you need the version diff --git a/owrx/source/hackrf.py b/owrx/source/hackrf.py index f59a448..02547f7 100644 --- a/owrx/source/hackrf.py +++ b/owrx/source/hackrf.py @@ -1,23 +1,8 @@ -from .direct import DirectSource +from .soapy import SoapyConnectorSource from owrx.command import Option import time -class HackrfSource(DirectSource): - def getCommandMapper(self): - return super().getCommandMapper().setBase("hackrf_transfer").setMappings( - { - "samp_rate": Option("-s"), - "tuner_freq": Option("-f"), - "rf_gain": Option("-g"), - "lna_gain": Option("-l"), - "rf_amp": Option("-a"), - "ppm": Option("-C"), - } - ).setStatic("-r-") - - def getFormatConversion(self): - return ["csdr convert_s8_f"] - - def sleepOnRestart(self): - time.sleep(1) +class HackrfSource(SoapyConnectorSource): + def getDriver(self): + return "hackrf" \ No newline at end of file From bfe6c00f90770fed54344a462dc3f64a86dbc87c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 30 May 2020 22:59:45 +0200 Subject: [PATCH 365/475] add debian changelog entry, too --- debian/changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/debian/changelog b/debian/changelog index f8b4c6c..d5a6434 100644 --- a/debian/changelog +++ b/debian/changelog @@ -23,6 +23,7 @@ openwebrx (0.19.0) UNRELEASED; urgency=low installation of js8call and the js8py library) * Reorganization of the frontend demodulator code * Improve receiver load time by concatenating javascript assets + * HackRF support is now based on SoapyHackRF -- Jakob Ketterl Thu, 20 Feb 2020 21:01:00 +0000 From 3371697e1886d04a47940c0c67a9b3172824ba89 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 30 May 2020 23:03:43 +0200 Subject: [PATCH 366/475] add bias_tee mapping --- owrx/source/hackrf.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/owrx/source/hackrf.py b/owrx/source/hackrf.py index 02547f7..a103218 100644 --- a/owrx/source/hackrf.py +++ b/owrx/source/hackrf.py @@ -1,8 +1,11 @@ from .soapy import SoapyConnectorSource -from owrx.command import Option -import time class HackrfSource(SoapyConnectorSource): + def getSoapySettingsMappings(self): + mappings = super().getSoapySettingsMappings() + mappings.update({"bias_tee": "bias_tx"}) + return mappings + def getDriver(self): return "hackrf" \ No newline at end of file From 76818302560ff0963f6dd25a1a7f61ec72a251c4 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sat, 30 May 2020 23:41:30 +0200 Subject: [PATCH 367/475] add soapy module for hackrf --- docker/Dockerfiles/Dockerfile-full | 2 +- docker/Dockerfiles/Dockerfile-hackrf | 2 +- docker/scripts/install-dependencies-hackrf.sh | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfiles/Dockerfile-full b/docker/Dockerfiles/Dockerfile-full index fbb3bd7..af5007f 100644 --- a/docker/Dockerfiles/Dockerfile-full +++ b/docker/Dockerfiles/Dockerfile-full @@ -5,8 +5,8 @@ ADD docker/scripts/install-dependencies-*.sh / ADD docker/files/sdrplay/install-lib.*.patch / RUN /install-dependencies-rtlsdr.sh &&\ - /install-dependencies-hackrf.sh &&\ /install-dependencies-soapysdr.sh &&\ + /install-dependencies-hackrf.sh &&\ /install-dependencies-sdrplay.sh &&\ /install-dependencies-airspy.sh &&\ /install-dependencies-rtlsdr-soapy.sh &&\ diff --git a/docker/Dockerfiles/Dockerfile-hackrf b/docker/Dockerfiles/Dockerfile-hackrf index 6d5d5e8..93cb35b 100644 --- a/docker/Dockerfiles/Dockerfile-hackrf +++ b/docker/Dockerfiles/Dockerfile-hackrf @@ -1,5 +1,5 @@ ARG ARCHTAG -FROM openwebrx-base:$ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG ADD docker/scripts/install-dependencies-hackrf.sh / RUN /install-dependencies-hackrf.sh &&\ diff --git a/docker/scripts/install-dependencies-hackrf.sh b/docker/scripts/install-dependencies-hackrf.sh index 9c6d47b..d01d009 100755 --- a/docker/scripts/install-dependencies-hackrf.sh +++ b/docker/scripts/install-dependencies-hackrf.sh @@ -31,6 +31,9 @@ cmakebuild host cd .. rm -rf hackrf +git clone https://github.com/pothosware/SoapyHackRF.git +cmakebuild SoapyHackRF 3c514cecd3dd2fdf4794aebc96c482940c11d7ff + SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES apt-get clean rm -rf /var/lib/apt/lists/* From 063d22f88c4a831902445c36412c7c287d322d99 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 31 May 2020 00:55:21 +0200 Subject: [PATCH 368/475] build with lime SIMD limited to SSE3 for better portability, refs #38 --- docker/scripts/install-dependencies-limesdr.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/scripts/install-dependencies-limesdr.sh b/docker/scripts/install-dependencies-limesdr.sh index abfa393..63f1c35 100755 --- a/docker/scripts/install-dependencies-limesdr.sh +++ b/docker/scripts/install-dependencies-limesdr.sh @@ -15,7 +15,7 @@ cd LimeSuite git checkout 0854a51ec06b30b01f19a562149c39461e92f24d mkdir builddir cd builddir -cmake .. -DENABLE_EXAMPLES=OFF -DENABLE_DESKTOP=OFF -DENABLE_LIME_UTIL=OFF -DENABLE_QUICKTEST=OFF -DENABLE_OCTAVE=OFF -DENABLE_GUI=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-latomic" +cmake .. -DENABLE_EXAMPLES=OFF -DENABLE_DESKTOP=OFF -DENABLE_LIME_UTIL=OFF -DENABLE_QUICKTEST=OFF -DENABLE_OCTAVE=OFF -DENABLE_GUI=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-latomic" -DDEFAULT_SIMD_FLAGS=SSE3 make make install cd ../.. From 1359da5b14a55580364511a7cf9f6197ef466a8d Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 31 May 2020 01:04:57 +0200 Subject: [PATCH 369/475] limit SIMD flags to x86 only --- docker/scripts/install-dependencies-limesdr.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docker/scripts/install-dependencies-limesdr.sh b/docker/scripts/install-dependencies-limesdr.sh index 63f1c35..be7533e 100755 --- a/docker/scripts/install-dependencies-limesdr.sh +++ b/docker/scripts/install-dependencies-limesdr.sh @@ -10,12 +10,17 @@ BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++" apt-get update apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES +SIMD_FLAGS="" +if [[ 'x86_64' == `uname -m` ]] ; then + SIMD_FLAGS="-DDEFAULT_SIMD_FLAGS=SSE3" +fi + git clone https://github.com/myriadrf/LimeSuite.git cd LimeSuite git checkout 0854a51ec06b30b01f19a562149c39461e92f24d mkdir builddir cd builddir -cmake .. -DENABLE_EXAMPLES=OFF -DENABLE_DESKTOP=OFF -DENABLE_LIME_UTIL=OFF -DENABLE_QUICKTEST=OFF -DENABLE_OCTAVE=OFF -DENABLE_GUI=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-latomic" -DDEFAULT_SIMD_FLAGS=SSE3 +cmake .. -DENABLE_EXAMPLES=OFF -DENABLE_DESKTOP=OFF -DENABLE_LIME_UTIL=OFF -DENABLE_QUICKTEST=OFF -DENABLE_OCTAVE=OFF -DENABLE_GUI=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-latomic" ${SIMD_FLAGS} make make install cd ../.. From 9763f302f3215aa7c4d3428216c8611579f63397 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 31 May 2020 15:04:09 +0200 Subject: [PATCH 370/475] switch to csdr and owrx_connector development versions --- docker/scripts/install-connectors.sh | 2 +- docker/scripts/install-dependencies.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/scripts/install-connectors.sh b/docker/scripts/install-connectors.sh index 3f5861b..d7014e7 100755 --- a/docker/scripts/install-connectors.sh +++ b/docker/scripts/install-connectors.sh @@ -24,7 +24,7 @@ apt-get update apt-get -y install --no-install-recommends $BUILD_PACKAGES git clone https://github.com/jketterl/owrx_connector.git -cmakebuild owrx_connector 4cb8d14fbe387b1569a5b635d7819266ce1dd42b +cmakebuild owrx_connector 45ec227b38bb763b0a923a1856740f4ddf74216c apt-get -y purge --autoremove $BUILD_PACKAGES apt-get clean diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh index f977430..fc76cfa 100755 --- a/docker/scripts/install-dependencies.sh +++ b/docker/scripts/install-dependencies.sh @@ -55,7 +55,7 @@ cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d git clone https://github.com/jketterl/csdr.git cd csdr -git checkout 69c4d74a5b8207b0edf4a36a5a0795fbee39281f +git checkout c4d8a8a5590898e8c9e94b88b96a2fdc7cd0493a autoreconf -i ./configure make From 66b5f17d38ac18bae2f7e88ccfbc8b7fb6b7e19c Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 31 May 2020 19:57:20 +0200 Subject: [PATCH 371/475] implement soapy gain input --- htdocs/settings.js | 52 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/htdocs/settings.js b/htdocs/settings.js index 278e1c0..bf9a03c 100644 --- a/htdocs/settings.js +++ b/htdocs/settings.js @@ -5,13 +5,19 @@ function Input(name, value, options) { this.label = options && options.label || name; }; +Input.prototype.getClasses = function() { + return ['form-control', 'form-control-sm']; +} + Input.prototype.bootstrapify = function(input) { - input.addClass('form-control').addClass('form-control-sm'); + this.getClasses().forEach(input.addClass.bind(input)); return [ '
', '', '
', - input[0].outerHTML, + $.map(input, function(el) { + return el.outerHTML; + }).join(''), '
', '
' ].join(''); @@ -43,8 +49,48 @@ function SoapyGainInput() { SoapyGainInput.prototype = new Input(); +SoapyGainInput.prototype.getClasses = function() { + return []; +}; + SoapyGainInput.prototype.render = function(){ - return this.bootstrapify($('
Soapy gain settings go here
')); + var markup = $( + '
' + + '
Gain mode
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
Gain
' + + '
' + + '' + + '
' + + '
' + + this.options.gains.map(function(g){ + return '
' + + '
' + g + '
' + + '
' + + '' + + '
' + + '
'; + }).join('') + ); + var el = $(this.bootstrapify(markup)) + var setMode = function(mode){ + el.find('.option').hide(); + el.find('.gain-mode-' + mode).show(); + }; + el.on('change', 'select', function(){ + var mode = $(this).val(); + setMode(mode); + }); + setMode('auto'); + return el; }; function ProfileInput() { From ac18a76c14e24b463305312a9dbc825af0d52a56 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 31 May 2020 20:25:41 +0200 Subject: [PATCH 372/475] split stuff into separate files --- htdocs/generalsettings.html | 4 +- htdocs/lib/settings/Input.js | 114 +++++++++++ htdocs/lib/settings/SdrDevice.js | 226 ++++++++++++++++++++ htdocs/sdrsettings.html | 4 +- htdocs/settings.html | 4 +- htdocs/settings.js | 342 ------------------------------- owrx/controllers/assets.py | 7 + 7 files changed, 350 insertions(+), 351 deletions(-) create mode 100644 htdocs/lib/settings/Input.js create mode 100644 htdocs/lib/settings/SdrDevice.js diff --git a/htdocs/generalsettings.html b/htdocs/generalsettings.html index 3367ed8..4731a02 100644 --- a/htdocs/generalsettings.html +++ b/htdocs/generalsettings.html @@ -5,10 +5,8 @@ - - - + diff --git a/htdocs/lib/settings/Input.js b/htdocs/lib/settings/Input.js new file mode 100644 index 0000000..4686aac --- /dev/null +++ b/htdocs/lib/settings/Input.js @@ -0,0 +1,114 @@ +function Input(name, value, options) { + this.name = name; + this.value = value; + this.options = options; + this.label = options && options.label || name; +}; + +Input.prototype.getClasses = function() { + return ['form-control', 'form-control-sm']; +} + +Input.prototype.bootstrapify = function(input) { + this.getClasses().forEach(input.addClass.bind(input)); + return [ + '
', + '', + '
', + $.map(input, function(el) { + return el.outerHTML; + }).join(''), + '
', + '
' + ].join(''); +}; + +function TextInput() { + Input.apply(this, arguments); +}; + +TextInput.prototype = new Input(); + +TextInput.prototype.render = function() { + return this.bootstrapify($('')); +} + +function NumberInput() { + Input.apply(this, arguments); +}; + +NumberInput.prototype = new Input(); + +NumberInput.prototype.render = function() { + return this.bootstrapify($('')); +}; + +function SoapyGainInput() { + Input.apply(this, arguments); +} + +SoapyGainInput.prototype = new Input(); + +SoapyGainInput.prototype.getClasses = function() { + return []; +}; + +SoapyGainInput.prototype.render = function(){ + var markup = $( + '
' + + '
Gain mode
' + + '
' + + '' + + '
' + + '
' + + '
' + + '
Gain
' + + '
' + + '' + + '
' + + '
' + + this.options.gains.map(function(g){ + return '
' + + '
' + g + '
' + + '
' + + '' + + '
' + + '
'; + }).join('') + ); + var el = $(this.bootstrapify(markup)) + var setMode = function(mode){ + el.find('.option').hide(); + el.find('.gain-mode-' + mode).show(); + }; + el.on('change', 'select', function(){ + var mode = $(this).val(); + setMode(mode); + }); + setMode('auto'); + return el; +}; + +function ProfileInput() { + Input.apply(this, arguments); +}; + +ProfileInput.prototype = new Input(); + +ProfileInput.prototype.render = function() { + return $('

Profiles

'); +}; + +function SchedulerInput() { + Input.apply(this, arguments); +}; + +SchedulerInput.prototype = new Input(); + +SchedulerInput.prototype.render = function() { + return $('

Scheduler

'); +}; diff --git a/htdocs/lib/settings/SdrDevice.js b/htdocs/lib/settings/SdrDevice.js new file mode 100644 index 0000000..f8acada --- /dev/null +++ b/htdocs/lib/settings/SdrDevice.js @@ -0,0 +1,226 @@ +function SdrDevice(el, data) { + this.el = el; + this.data = data; + this.inputs = {}; + this.render(); + + var self = this; + el.on('click', '.fieldselector .btn', function() { + var key = el.find('.fieldselector select').val(); + self.data[key] = self.getInitialValue(key); + self.render(); + }); +}; + +SdrDevice.create = function(el) { + var data = JSON.parse(decodeURIComponent(el.data('config'))); + var type = data.type; + var constructor = SdrDevice.types[type] || SdrDevice; + return new constructor(el, data); +}; + +SdrDevice.prototype.getData = function() { + return $.extend(new Object(), this.getDefaults(), this.data); +}; + +SdrDevice.prototype.getDefaults = function() { + var defaults = {} + $.each(this.getMappings(), function(k, v) { + if (!v.includeInDefault) return; + defaults[k] = 'initialValue' in v ? v['initialValue'] : false; + }); + return defaults; +}; + +SdrDevice.prototype.getMappings = function() { + return { + "name": { + constructor: TextInput, + inputOptions: { + label: "Name" + }, + initialValue: "", + includeInDefault: true + }, + "type": { + constructor: TextInput, + inputOptions: { + label: "Type" + }, + initialValue: '', + includeInDefault: true + }, + "ppm": { + constructor: NumberInput, + inputOptions: { + label: "PPM" + }, + initialValue: 0 + }, + "profiles": { + constructor: ProfileInput, + inputOptions: { + label: "Profiles" + }, + initialValue: [], + includeInDefault: true, + position: 100 + }, + "scheduler": { + constructor: SchedulerInput, + inputOptions: { + label: "Scheduler", + }, + initialValue: {}, + position: 101 + }, + "rf_gain": { + constructor: TextInput, + inputOptions: { + label: "Gain", + }, + initialValue: 0 + } + }; +}; + +SdrDevice.prototype.getMapping = function(key) { + var mappings = this.getMappings(); + return mappings[key]; +}; + +SdrDevice.prototype.getInputClass = function(key) { + var mapping = this.getMapping(key); + return mapping && mapping.constructor || TextInput; +}; + +SdrDevice.prototype.getInitialValue = function(key) { + var mapping = this.getMapping(key); + return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false; +}; + +SdrDevice.prototype.getPosition = function(key) { + var mapping = this.getMapping(key); + return mapping && mapping.position || 10; +}; + +SdrDevice.prototype.getInputOptions = function(key) { + var mapping = this.getMapping(key); + return mapping && mapping.inputOptions || {}; +}; + +SdrDevice.prototype.getLabel = function(key) { + var options = this.getInputOptions(key); + return options && options.label || key; +}; + +SdrDevice.prototype.render = function() { + var self = this; + self.el.empty(); + var data = this.getData(); + Object.keys(data).sort(function(a, b){ + return self.getPosition(a) - self.getPosition(b); + }).forEach(function(key){ + var value = data[key]; + var inputClass = self.getInputClass(key); + var input = new inputClass(key, value, self.getInputOptions(key)); + self.inputs[key] = input; + self.el.append(input.render()); + }); + self.el.append(this.renderFieldSelector()); +}; + +SdrDevice.prototype.renderFieldSelector = function() { + var self = this; + return '
' + + '

Add new configuration options

' + + '
' + + '
' + + '
' + + '
Add to config
' + + '
' + + '
' + + '

'; +}; + +RtlSdrDevice = function() { + SdrDevice.apply(this, arguments); +}; + +RtlSdrDevice.prototype = Object.create(SdrDevice.prototype); +RtlSdrDevice.prototype.constructor = RtlSdrDevice; + +RtlSdrDevice.prototype.getMappings = function() { + var mappings = SdrDevice.prototype.getMappings.apply(this, arguments); + return $.extend(new Object(), mappings, { + "device": { + constructor: TextInput, + inputOptions:{ + label: "Serial number" + }, + initialValue: "" + } + }); +}; + +SoapySdrDevice = function() { + SdrDevice.apply(this, arguments); +}; + +SoapySdrDevice.prototype = Object.create(SdrDevice.prototype); +SoapySdrDevice.prototype.constructor = SoapySdrDevice; + +SoapySdrDevice.prototype.getMappings = function() { + var mappings = SdrDevice.prototype.getMappings.apply(this, arguments); + return $.extend(new Object(), mappings, { + "device": { + constructor: TextInput, + inputOptions:{ + label: "Soapy device selector" + }, + initialValue: "" + } + }); +}; + +SdrplaySdrDevice = function() { + SoapySdrDevice.apply(this, arguments); +}; + +SdrplaySdrDevice.prototype = Object.create(SoapySdrDevice.prototype); +SdrplaySdrDevice.prototype.constructor = SdrplaySdrDevice; + +SdrplaySdrDevice.prototype.getMappings = function() { + var mappings = SoapySdrDevice.prototype.getMappings.apply(this, arguments); + return $.extend(new Object(), mappings, { + "rf_gain": { + constructor: SoapyGainInput, + initialValue: 0, + inputOptions: { + label: "Gain", + gains: ['RFGR', 'IFGR'] + } + } + }); +}; + +SdrDevice.types = { + 'rtl_sdr': RtlSdrDevice, + 'sdrplay': SdrplaySdrDevice +}; + +$.fn.sdrdevice = function() { + return this.map(function(){ + var el = $(this); + if (!el.data('sdrdevice')) { + el.data('sdrdevice', SdrDevice.create(el)); + } + return el.data('sdrdevice'); + }); +}; diff --git a/htdocs/sdrsettings.html b/htdocs/sdrsettings.html index 89e6a8f..74aa8b7 100644 --- a/htdocs/sdrsettings.html +++ b/htdocs/sdrsettings.html @@ -5,9 +5,7 @@ - - - + diff --git a/htdocs/settings.html b/htdocs/settings.html index 39cecf9..80dce91 100644 --- a/htdocs/settings.html +++ b/htdocs/settings.html @@ -5,9 +5,7 @@ - - - + diff --git a/htdocs/settings.js b/htdocs/settings.js index bf9a03c..e95e2fe 100644 --- a/htdocs/settings.js +++ b/htdocs/settings.js @@ -1,345 +1,3 @@ -function Input(name, value, options) { - this.name = name; - this.value = value; - this.options = options; - this.label = options && options.label || name; -}; - -Input.prototype.getClasses = function() { - return ['form-control', 'form-control-sm']; -} - -Input.prototype.bootstrapify = function(input) { - this.getClasses().forEach(input.addClass.bind(input)); - return [ - '
', - '', - '
', - $.map(input, function(el) { - return el.outerHTML; - }).join(''), - '
', - '
' - ].join(''); -}; - -function TextInput() { - Input.apply(this, arguments); -}; - -TextInput.prototype = new Input(); - -TextInput.prototype.render = function() { - return this.bootstrapify($('')); -} - -function NumberInput() { - Input.apply(this, arguments); -}; - -NumberInput.prototype = new Input(); - -NumberInput.prototype.render = function() { - return this.bootstrapify($('')); -}; - -function SoapyGainInput() { - Input.apply(this, arguments); -} - -SoapyGainInput.prototype = new Input(); - -SoapyGainInput.prototype.getClasses = function() { - return []; -}; - -SoapyGainInput.prototype.render = function(){ - var markup = $( - '
' + - '
Gain mode
' + - '
' + - '' + - '
' + - '
' + - '
' + - '
Gain
' + - '
' + - '' + - '
' + - '
' + - this.options.gains.map(function(g){ - return '
' + - '
' + g + '
' + - '
' + - '' + - '
' + - '
'; - }).join('') - ); - var el = $(this.bootstrapify(markup)) - var setMode = function(mode){ - el.find('.option').hide(); - el.find('.gain-mode-' + mode).show(); - }; - el.on('change', 'select', function(){ - var mode = $(this).val(); - setMode(mode); - }); - setMode('auto'); - return el; -}; - -function ProfileInput() { - Input.apply(this, arguments); -}; - -ProfileInput.prototype = new Input(); - -ProfileInput.prototype.render = function() { - return $('

Profiles

'); -}; - -function SchedulerInput() { - Input.apply(this, arguments); -}; - -SchedulerInput.prototype = new Input(); - -SchedulerInput.prototype.render = function() { - return $('

Scheduler

'); -}; - -function SdrDevice(el, data) { - this.el = el; - this.data = data; - this.inputs = {}; - this.render(); - - var self = this; - el.on('click', '.fieldselector .btn', function() { - var key = el.find('.fieldselector select').val(); - self.data[key] = self.getInitialValue(key); - self.render(); - }); -}; - -SdrDevice.create = function(el) { - var data = JSON.parse(decodeURIComponent(el.data('config'))); - var type = data.type; - var constructor = SdrDevice.types[type] || SdrDevice; - return new constructor(el, data); -}; - -SdrDevice.prototype.getData = function() { - return $.extend(new Object(), this.getDefaults(), this.data); -}; - -SdrDevice.prototype.getDefaults = function() { - var defaults = {} - $.each(this.getMappings(), function(k, v) { - if (!v.includeInDefault) return; - defaults[k] = 'initialValue' in v ? v['initialValue'] : false; - }); - return defaults; -}; - -SdrDevice.prototype.getMappings = function() { - return { - "name": { - constructor: TextInput, - inputOptions: { - label: "Name" - }, - initialValue: "", - includeInDefault: true - }, - "type": { - constructor: TextInput, - inputOptions: { - label: "Type" - }, - initialValue: '', - includeInDefault: true - }, - "ppm": { - constructor: NumberInput, - inputOptions: { - label: "PPM" - }, - initialValue: 0 - }, - "profiles": { - constructor: ProfileInput, - inputOptions: { - label: "Profiles" - }, - initialValue: [], - includeInDefault: true, - position: 100 - }, - "scheduler": { - constructor: SchedulerInput, - inputOptions: { - label: "Scheduler", - }, - initialValue: {}, - position: 101 - }, - "rf_gain": { - constructor: TextInput, - inputOptions: { - label: "Gain", - }, - initialValue: 0 - } - }; -}; - -SdrDevice.prototype.getMapping = function(key) { - var mappings = this.getMappings(); - return mappings[key]; -}; - -SdrDevice.prototype.getInputClass = function(key) { - var mapping = this.getMapping(key); - return mapping && mapping.constructor || TextInput; -}; - -SdrDevice.prototype.getInitialValue = function(key) { - var mapping = this.getMapping(key); - return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false; -}; - -SdrDevice.prototype.getPosition = function(key) { - var mapping = this.getMapping(key); - return mapping && mapping.position || 10; -}; - -SdrDevice.prototype.getInputOptions = function(key) { - var mapping = this.getMapping(key); - return mapping && mapping.inputOptions || {}; -}; - -SdrDevice.prototype.getLabel = function(key) { - var options = this.getInputOptions(key); - return options && options.label || key; -}; - -SdrDevice.prototype.render = function() { - var self = this; - self.el.empty(); - var data = this.getData(); - Object.keys(data).sort(function(a, b){ - return self.getPosition(a) - self.getPosition(b); - }).forEach(function(key){ - var value = data[key]; - var inputClass = self.getInputClass(key); - var input = new inputClass(key, value, self.getInputOptions(key)); - self.inputs[key] = input; - self.el.append(input.render()); - }); - self.el.append(this.renderFieldSelector()); -}; - -SdrDevice.prototype.renderFieldSelector = function() { - var self = this; - return '
' + - '

Add new configuration options

' + - '
' + - '
' + - '
' + - '
Add to config
' + - '
' + - '
' + - '

'; -}; - -RtlSdrDevice = function() { - SdrDevice.apply(this, arguments); -}; - -RtlSdrDevice.prototype = Object.create(SdrDevice.prototype); -RtlSdrDevice.prototype.constructor = RtlSdrDevice; - -RtlSdrDevice.prototype.getMappings = function() { - var mappings = SdrDevice.prototype.getMappings.apply(this, arguments); - return $.extend(new Object(), mappings, { - "device": { - constructor: TextInput, - inputOptions:{ - label: "Serial number" - }, - initialValue: "" - } - }); -}; - -SoapySdrDevice = function() { - SdrDevice.apply(this, arguments); -}; - -SoapySdrDevice.prototype = Object.create(SdrDevice.prototype); -SoapySdrDevice.prototype.constructor = SoapySdrDevice; - -SoapySdrDevice.prototype.getMappings = function() { - var mappings = SdrDevice.prototype.getMappings.apply(this, arguments); - return $.extend(new Object(), mappings, { - "device": { - constructor: TextInput, - inputOptions:{ - label: "Soapy device selector" - }, - initialValue: "" - } - }); -}; - -SdrplaySdrDevice = function() { - SoapySdrDevice.apply(this, arguments); -}; - -SdrplaySdrDevice.prototype = Object.create(SoapySdrDevice.prototype); -SdrplaySdrDevice.prototype.constructor = SdrplaySdrDevice; - -SdrplaySdrDevice.prototype.getMappings = function() { - var mappings = SoapySdrDevice.prototype.getMappings.apply(this, arguments); - return $.extend(new Object(), mappings, { - "rf_gain": { - constructor: SoapyGainInput, - initialValue: 0, - inputOptions: { - label: "Gain", - gains: ['RFGR', 'IFGR'] - } - } - }); -}; - -SdrDevice.types = { - 'rtl_sdr': RtlSdrDevice, - 'sdrplay': SdrplaySdrDevice -}; - -$.fn.sdrdevice = function() { - return this.map(function(){ - var el = $(this); - if (!el.data('sdrdevice')) { - el.data('sdrdevice', SdrDevice.create(el)); - } - return el.data('sdrdevice'); - }); -}; - $(function(){ $(".map-input").each(function(el) { var $el = $(this); diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py index 59a532f..a8b9d4e 100644 --- a/owrx/controllers/assets.py +++ b/owrx/controllers/assets.py @@ -87,6 +87,13 @@ class CompiledAssetsController(Controller): "lib/Header.js", "map.js", ], + "settings.js": [ + "lib/jquery-3.2.1.min.js", + "lib/Header.js", + "lib/settings/Input.js", + "lib/settings/SdrDevice.js", + "settings.js", + ] } def indexAction(self): From d3a307850467c6e54470c72660947647da94dbd8 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 31 May 2020 20:43:12 +0200 Subject: [PATCH 373/475] soapy gain input box for airspyhf --- htdocs/lib/settings/SdrDevice.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/htdocs/lib/settings/SdrDevice.js b/htdocs/lib/settings/SdrDevice.js index f8acada..79c970b 100644 --- a/htdocs/lib/settings/SdrDevice.js +++ b/htdocs/lib/settings/SdrDevice.js @@ -210,9 +210,31 @@ SdrplaySdrDevice.prototype.getMappings = function() { }); }; +AirspyHfSdrDevice = function() { + SoapySdrDevice.apply(this, arguments); +}; + +AirspyHfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype); +AirspyHfSdrDevice.prototype.constructor = AirspyHfSdrDevice; + +AirspyHfSdrDevice.prototype.getMappings = function() { + var mappings = SoapySdrDevice.prototype.getMappings.apply(this, arguments); + return $.extend(new Object(), mappings, { + "rf_gain": { + constructor: SoapyGainInput, + initialValue: 0, + inputOptions: { + label: "Gain", + gains: ['RF', 'VGA'] + } + } + }); +}; + SdrDevice.types = { 'rtl_sdr': RtlSdrDevice, - 'sdrplay': SdrplaySdrDevice + 'sdrplay': SdrplaySdrDevice, + 'airspyhf': AirspyHfSdrDevice }; $.fn.sdrdevice = function() { From adcac7b54af4c372efaf15ee4f7e6a4c9e0faeec Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 31 May 2020 20:52:45 +0200 Subject: [PATCH 374/475] hackrf gain settings --- htdocs/lib/settings/SdrDevice.js | 54 +++++++++++++++++--------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/htdocs/lib/settings/SdrDevice.js b/htdocs/lib/settings/SdrDevice.js index 79c970b..25f85c9 100644 --- a/htdocs/lib/settings/SdrDevice.js +++ b/htdocs/lib/settings/SdrDevice.js @@ -185,10 +185,22 @@ SoapySdrDevice.prototype.getMappings = function() { label: "Soapy device selector" }, initialValue: "" + }, + "rf_gain": { + constructor: SoapyGainInput, + initialValue: 0, + inputOptions: { + label: "Gain", + gains: this.getGains() + } } }); }; +SoapySdrDevice.prototype.getGains = function() { + return []; +}; + SdrplaySdrDevice = function() { SoapySdrDevice.apply(this, arguments); }; @@ -196,18 +208,8 @@ SdrplaySdrDevice = function() { SdrplaySdrDevice.prototype = Object.create(SoapySdrDevice.prototype); SdrplaySdrDevice.prototype.constructor = SdrplaySdrDevice; -SdrplaySdrDevice.prototype.getMappings = function() { - var mappings = SoapySdrDevice.prototype.getMappings.apply(this, arguments); - return $.extend(new Object(), mappings, { - "rf_gain": { - constructor: SoapyGainInput, - initialValue: 0, - inputOptions: { - label: "Gain", - gains: ['RFGR', 'IFGR'] - } - } - }); +SdrplaySdrDevice.prototype.getGains = function() { + return ['RFGR', 'IFGR']; }; AirspyHfSdrDevice = function() { @@ -217,24 +219,26 @@ AirspyHfSdrDevice = function() { AirspyHfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype); AirspyHfSdrDevice.prototype.constructor = AirspyHfSdrDevice; -AirspyHfSdrDevice.prototype.getMappings = function() { - var mappings = SoapySdrDevice.prototype.getMappings.apply(this, arguments); - return $.extend(new Object(), mappings, { - "rf_gain": { - constructor: SoapyGainInput, - initialValue: 0, - inputOptions: { - label: "Gain", - gains: ['RF', 'VGA'] - } - } - }); +AirspyHfSdrDevice.prototype.getGains = function() { + return ['RF', 'VGA']; +}; + +HackRfSdrDevice = function() { + SoapySdrDevice.apply(this, arguments); +}; + +HackRfSdrDevice.prototype = Object.create(SoapySdrDevice.prototype); +HackRfSdrDevice.prototype.constructor = HackRfSdrDevice; + +HackRfSdrDevice.prototype.getGains = function() { + return ['LNA', 'VGA', 'AMP']; }; SdrDevice.types = { 'rtl_sdr': RtlSdrDevice, 'sdrplay': SdrplaySdrDevice, - 'airspyhf': AirspyHfSdrDevice + 'airspyhf': AirspyHfSdrDevice, + 'hackrf': HackRfSdrDevice }; $.fn.sdrdevice = function() { From 1019ed5793d495f8316217a08f408ef8aa46ca01 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Sun, 31 May 2020 21:24:07 +0200 Subject: [PATCH 375/475] fill gain input with values --- htdocs/lib/settings/Input.js | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/htdocs/lib/settings/Input.js b/htdocs/lib/settings/Input.js index 4686aac..f638257 100644 --- a/htdocs/lib/settings/Input.js +++ b/htdocs/lib/settings/Input.js @@ -75,13 +75,14 @@ SoapyGainInput.prototype.render = function(){ return '
' + '
' + g + '
' + '
' + - '' + + '' + '
' + '
'; }).join('') ); var el = $(this.bootstrapify(markup)) var setMode = function(mode){ + el.find('select').val(mode); el.find('.option').hide(); el.find('.gain-mode-' + mode).show(); }; @@ -89,7 +90,30 @@ SoapyGainInput.prototype.render = function(){ var mode = $(this).val(); setMode(mode); }); - setMode('auto'); + if (typeof(this.value) === 'number') { + setMode('single'); + el.find('.gain-mode-single input').val(this.value); + } else if (typeof(this.value) === 'string') { + if (this.value === 'auto') { + setMode('auto'); + } else { + setMode('separate'); + values = $.extend.apply($, this.value.split(',').map(function(seg){ + var split = seg.split('='); + if (split.length < 2) return; + var res = {}; + res[split[0]] = parseInt(split[1]); + return res; + })); + el.find('.gain-mode-separate input').each(function(){ + var $input = $(this); + var g = $input.data('gain'); + $input.val(g in values ? values[g] : 0); + }); + } + } else { + setMode('auto'); + } return el; }; From ebd1e04414bc5e492de534ab0611b608f731cf73 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jun 2020 15:58:15 +0200 Subject: [PATCH 376/475] remove sdr.hu parts --- config_webrx.py | 9 -------- owrx/__main__.py | 5 ----- owrx/controllers/settings.py | 12 ---------- owrx/sdrhu.py | 43 ------------------------------------ sdrhu.py | 40 --------------------------------- 5 files changed, 109 deletions(-) delete mode 100644 owrx/sdrhu.py delete mode 100755 sdrhu.py diff --git a/config_webrx.py b/config_webrx.py index 15b96e4..c379749 100644 --- a/config_webrx.py +++ b/config_webrx.py @@ -59,15 +59,6 @@ Antenna: Receiver Antenna
Website: http://localhost """ -# ==== sdr.hu listing ==== -# If you want your ham receiver to be listed publicly on sdr.hu, then take the following steps: -# 1. Register at: http://sdr.hu/register -# 2. You will get an unique key by email. Copy it and paste here: -sdrhu_key = "" -# 3. Set this setting to True to enable listing: -sdrhu_public_listing = False -server_hostname = "localhost" - # ==== DSP/RX settings ==== fft_fps = 9 fft_size = 4096 # Should be power of 2 diff --git a/owrx/__main__.py b/owrx/__main__.py index 84bca23..2bf3ec9 100644 --- a/owrx/__main__.py +++ b/owrx/__main__.py @@ -9,7 +9,6 @@ from owrx.config import Config from owrx.feature import FeatureDetector from owrx.sdr import SdrService from socketserver import ThreadingMixIn -from owrx.sdrhu import SdrHuUpdater from owrx.service import Services from owrx.websocket import WebSocketConnection from owrx.pskreporter import PskReporter @@ -59,10 +58,6 @@ Support and info: https://groups.io/g/openwebrx # Get error messages about unknown / unavailable features as soon as possible SdrService.loadProps() - if "sdrhu_key" in pm and pm["sdrhu_public_listing"]: - updater = SdrHuUpdater() - updater.start() - Services.start() try: diff --git a/owrx/controllers/settings.py b/owrx/controllers/settings.py index 1eaceed..368a167 100644 --- a/owrx/controllers/settings.py +++ b/owrx/controllers/settings.py @@ -244,18 +244,6 @@ class GeneralSettingsController(AdminController): infotext="This callsign will be used to send spots to pskreporter.info", ), ), - Section( - "sdr.hu", - TextInput( - "sdrhu_key", - "sdr.hu key", - infotext='Please obtain your personal key on sdr.hu', - ), - CheckboxInput( - "sdrhu_public_listing", "List on sdr.hu", "List my receiver on sdr.hu" - ), - TextInput("server_hostname", "Hostname"), - ), ] def render_sections(self): diff --git a/owrx/sdrhu.py b/owrx/sdrhu.py deleted file mode 100644 index 193399f..0000000 --- a/owrx/sdrhu.py +++ /dev/null @@ -1,43 +0,0 @@ -import threading -import time -from owrx.config import Config -from urllib import request, parse - -import logging - -logger = logging.getLogger(__name__) - - -class SdrHuUpdater(threading.Thread): - def __init__(self): - self.doRun = True - super().__init__(daemon=True) - - def update(self): - pm = Config.get().filter("server_hostname", "web_port", "sdrhu_key") - data = parse.urlencode({ - "url": "http://{server_hostname}:{web_port}".format(**pm.__dict__()), - "apikey": pm["sdrhu_key"] - }).encode() - - res = request.urlopen("https://sdr.hu/update", data=data) - if res.getcode() < 200 or res.getcode() >= 300: - logger.warning('sdr.hu update failed with error code %i', res.getcode()) - return 2 - - returned = res.read().decode("utf-8") - if "UPDATE:" not in returned: - logger.warning("Update failed, your receiver cannot be listed on sdr.hu!") - return 2 - - value = returned.split("UPDATE:")[1].split("\n", 1)[0] - if value.startswith("SUCCESS"): - logger.info("Update succeeded!") - else: - logger.warning("Update failed, your receiver cannot be listed on sdr.hu! Reason: %s", value) - return 20 - - def run(self): - while self.doRun: - retrytime_mins = self.update() - time.sleep(60 * retrytime_mins) diff --git a/sdrhu.py b/sdrhu.py deleted file mode 100755 index 9678a9b..0000000 --- a/sdrhu.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/python3 -""" - - This file is part of OpenWebRX, - an open-source SDR receiver software with a web UI. - Copyright (c) 2013-2015 by Andras Retzler - Copyright (c) 2019-2020 by Jakob Ketterl - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -""" - -from owrx.sdrhu import SdrHuUpdater -from owrx.config import Config - -import logging -logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") -logger = logging.getLogger(__name__) - -if __name__ == "__main__": - pm = Config.get() - - if "sdrhu_public_listing" not in pm or not pm["sdrhu_public_listing"]: - logger.error('Public listing on sdr.hu is not activated. Please check "sdrhu_public_listing" in your config.') - exit(1) - if "sdrhu_key" not in pm or pm["sdrhu_key"] is None or pm["sdrhu_key"] == "": - logger.error('Missing "sdrhu_key" in your config. Aborting') - exit(1) - SdrHuUpdater().update() From c87cfed525d2dc245f7740b5bb0d6b47e083bbae Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jun 2020 16:03:22 +0200 Subject: [PATCH 377/475] remove old status urls --- owrx/controllers/status.py | 22 +--------------------- owrx/http.py | 5 +---- 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/owrx/controllers/status.py b/owrx/controllers/status.py index 61f7102..beded28 100644 --- a/owrx/controllers/status.py +++ b/owrx/controllers/status.py @@ -9,26 +9,6 @@ import pkg_resources class StatusController(Controller): - def indexAction(self): - pm = Config.get() - # convert to old format - gps = (pm["receiver_gps"]["lat"], pm["receiver_gps"]["lon"]) - avatar_path = pkg_resources.resource_filename("htdocs", "gfx/openwebrx-avatar.png") - # TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna - vars = { - "status": "active", - "name": pm["receiver_name"], - "op_email": pm["receiver_admin"], - "users": ClientRegistry.getSharedInstance().clientCount(), - "users_max": pm["max_clients"], - "gps": gps, - "asl": pm["receiver_asl"], - "loc": pm["receiver_location"], - "sw_version": openwebrx_version, - "avatar_ctime": os.path.getctime(avatar_path), - } - self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()])) - def getProfileStats(self, profile): return { "name": profile["name"], @@ -45,7 +25,7 @@ class StatusController(Controller): } return stats - def jsonAction(self): + def indexAction(self): pm = Config.get() status = { diff --git a/owrx/http.py b/owrx/http.py index 8381646..812bec0 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -89,15 +89,12 @@ class Router(object): def __init__(self): self.routes = [ StaticRoute("/", IndexController), - StaticRoute("/status", StatusController), - StaticRoute("/status.json", StatusController, options={"action": "jsonAction"}), + StaticRoute("/status.json", StatusController), RegexRoute("/static/(.+)", OwrxAssetsController), RegexRoute("/compiled/(.+)", CompiledAssetsController), RegexRoute("/aprs-symbols/(.+)", AprsSymbolsController), StaticRoute("/ws/", WebSocketController), RegexRoute("(/favicon.ico)", OwrxAssetsController), - # backwards compatibility for the sdr.hu portal - RegexRoute("(/gfx/openwebrx-avatar.png)", OwrxAssetsController), StaticRoute("/map", MapController), StaticRoute("/features", FeatureController), StaticRoute("/api/features", ApiController), From 835501a5f4b439709cbd7b4cc8b6eb58538c0ded Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jun 2020 18:15:03 +0200 Subject: [PATCH 378/475] update changelog --- CHANGELOG.md | 1 + debian/changelog | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f77877..972be94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ csdr and owrx_connector to allow the images to run on a wider range of CPUs - Docker containers have been updated to include the SDRplay driver version 3 - HackRF support is now based on SoapyHackRF +- Removed sdr.hu server listing support since the site has been shut down **0.18.0** - Support for SoapyRemote diff --git a/debian/changelog b/debian/changelog index d5a6434..d44716a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -24,6 +24,7 @@ openwebrx (0.19.0) UNRELEASED; urgency=low * Reorganization of the frontend demodulator code * Improve receiver load time by concatenating javascript assets * HackRF support is now based on SoapyHackRF + * Removed sdr.hu server listing support since the site has been shut down -- Jakob Ketterl Thu, 20 Feb 2020 21:01:00 +0000 From ea65ef010092f4f3fca727371af8a19d79b08da2 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jun 2020 18:34:54 +0200 Subject: [PATCH 379/475] update changelog --- CHANGELOG.md | 1 + debian/changelog | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 972be94..7fd020d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Docker containers have been updated to include the SDRplay driver version 3 - HackRF support is now based on SoapyHackRF - Removed sdr.hu server listing support since the site has been shut down +- Added support for Radioberry 2 Rasbperry Pi SDR Cape **0.18.0** - Support for SoapyRemote diff --git a/debian/changelog b/debian/changelog index d44716a..bb4363d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -25,6 +25,7 @@ openwebrx (0.19.0) UNRELEASED; urgency=low * Improve receiver load time by concatenating javascript assets * HackRF support is now based on SoapyHackRF * Removed sdr.hu server listing support since the site has been shut down + * Added support for Radioberry 2 Rasbperry Pi SDR Cape -- Jakob Ketterl Thu, 20 Feb 2020 21:01:00 +0000 From 16c59c324598c20078bf443ffb1eb79eacc62435 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jun 2020 19:05:09 +0200 Subject: [PATCH 380/475] release versions --- debian/changelog | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index bb4363d..ead4569 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -openwebrx (0.19.0) UNRELEASED; urgency=low +openwebrx (0.19.0) buster focal; urgency=low * Fix direwolf connection setup by implementing a retry loop * Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector * OSM maps instead of Google when google_maps_api_key is not set (thanks @@ -27,7 +27,7 @@ openwebrx (0.19.0) UNRELEASED; urgency=low * Removed sdr.hu server listing support since the site has been shut down * Added support for Radioberry 2 Rasbperry Pi SDR Cape - -- Jakob Ketterl Thu, 20 Feb 2020 21:01:00 +0000 + -- Jakob Ketterl Mon, 01 Jun 2020 17:02:00 +0000 openwebrx (0.18.0) buster; urgency=low From ae295d72ae1755a0b2ae9aa2954bab1be4707d26 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Mon, 1 Jun 2020 19:41:38 +0200 Subject: [PATCH 381/475] remove "under construction" notice --- htdocs/index.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/htdocs/index.html b/htdocs/index.html index 22f33bb..f0b2360 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -45,10 +45,6 @@

-
- Under construction -
We're working on the code right now, so the application might fail. -