Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sensor failure recovery, support calibration to the antenna, save onboard cals separately, cal on startup #281

Merged
merged 59 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
63f4dbd
add default differential cal file
aromanielloNTIA Jan 26, 2024
bac3109
remove sigan calibration
aromanielloNTIA Jan 26, 2024
4f150fa
update requirements
aromanielloNTIA Jan 26, 2024
a2353a4
update sensor loader for calibration changes
aromanielloNTIA Jan 26, 2024
03c6f66
Merge branch 'master' into calibrate_to_antenna
aromanielloNTIA Feb 7, 2024
c0eafb4
convert calibration file string to path before passing
aromanielloNTIA Feb 7, 2024
f6db82c
Merge branch 'master' into calibrate_to_antenna
aromanielloNTIA Feb 26, 2024
e0ff8a6
Group related sigan variables, add auto calibration config
aromanielloNTIA Mar 5, 2024
a9999c7
remove default calibration files
aromanielloNTIA Mar 5, 2024
caa811a
remove default calibration files and update cal loading
aromanielloNTIA Mar 5, 2024
073b1ef
add calibrate_on_startup
aromanielloNTIA Mar 5, 2024
5c343d2
avoid setting environment vars to None
aromanielloNTIA Mar 6, 2024
325b8bc
remove unused import
aromanielloNTIA Mar 6, 2024
bc5969c
fix sensor loader initialization
aromanielloNTIA Mar 6, 2024
b5bb922
get sensor cal from sensor instead of sigan
aromanielloNTIA Mar 6, 2024
e139738
Fix ONBOARD_CALIBRATION_FILE setting.
dboulware Mar 14, 2024
9576cbe
Add CALIBRATE_ON_STARTUP setting defaulting to false.
dboulware Mar 14, 2024
9175e88
Add CALIBRATE_ON_STARTUP to docker-compose.yml.
dboulware Mar 14, 2024
3829f7d
Use setting for STARTUP_CALIBRATION_ACTION and don't raise exception …
dboulware Mar 14, 2024
75e64c8
Set ONBOARD_CALIBRATION_FILE whether it exists or not.
dboulware Mar 14, 2024
33f8200
Cal on startup if calibration is expired.
dboulware Mar 14, 2024
bdf6b9f
Add CALIBRATION_EXPIRATION_LIMIT to docker-compose.yml and remove fr…
dboulware Mar 14, 2024
19ac7a4
Fix STARTUP_CALIBRATION_ACTION setting.
dboulware Mar 14, 2024
a47087f
Remove "CALIBRATION_EXPIRATION_LIMIT" from settings.
dboulware Mar 14, 2024
0bceffe
Only update cal when actually running.
dboulware Mar 14, 2024
d0c24cc
Merge in autohealing fixes. Move cal on startup check to initializati…
dboulware Mar 14, 2024
ec4c26a
remove actions from laod_sensor
dboulware Mar 14, 2024
ee5aef6
add RAY_INIT setting.
dboulware Mar 14, 2024
975d240
Log performing startup cal.
dboulware Mar 14, 2024
74bc22d
Correct log message.
dboulware Mar 15, 2024
103253a
temporarily catch BaseException from startup cal.
dboulware Mar 15, 2024
68a2aa5
create method for startup cal.
dboulware Mar 15, 2024
104ea45
move startup calibration into scheduler.
dboulware Mar 15, 2024
6d13cff
remove dead code.
dboulware Mar 15, 2024
c94399b
Don't raise exceptions (that will be caught anyway) in loading cals.
dboulware Mar 17, 2024
0b95fe9
pre-commit
dboulware Mar 19, 2024
acb3fe7
Merge branch 'master' into calibrate_to_antenna
aromanielloNTIA Mar 22, 2024
c096d36
update scos-tekrsa and scos-actions version
jhazentia Mar 27, 2024
3b06062
fix import
aromanielloNTIA Mar 27, 2024
c71267d
Remove obsolete 'version' field and rename to compose.yaml
aromanielloNTIA Mar 27, 2024
bd46397
GH Actions: wait longer for container to be healthy
aromanielloNTIA Mar 27, 2024
c9bc7d1
try longer sleep in GH actions testing
aromanielloNTIA Mar 27, 2024
d07afdb
use docker compose v2 in github action
aromanielloNTIA Mar 27, 2024
2864b66
return to shorter sleep time in GH action
aromanielloNTIA Mar 27, 2024
d4795de
skip startup calibration when running with mock sigan
aromanielloNTIA Mar 27, 2024
6b2b92a
add missing return statement
aromanielloNTIA Mar 27, 2024
a01a6ad
Do not check for USB_DEVICE when using mock sigan
aromanielloNTIA Mar 27, 2024
560e347
remove unused variables
aromanielloNTIA Mar 28, 2024
fbb8404
remove unused import, get SCOS_SENSOR_GIT_TAG from settings
aromanielloNTIA Mar 28, 2024
5680801
update readme for calibration changes
aromanielloNTIA Mar 28, 2024
fc8335b
remove unused variable CALIBRATE_ON_STARTUP
aromanielloNTIA Mar 28, 2024
e2bd008
switch BASE_IMAGE version to latest
aromanielloNTIA Mar 28, 2024
b0af772
update all pre-commit hooks
aromanielloNTIA Mar 28, 2024
c3f02ad
Run updated pre-commit on all files
aromanielloNTIA Mar 28, 2024
ec3cb11
Use get_disk_usage from SCOS Actions in status view
aromanielloNTIA Mar 28, 2024
beef855
remove unused import
aromanielloNTIA Mar 28, 2024
6da59db
fix wording in cal file comment
aromanielloNTIA Mar 28, 2024
2419998
simplify parameterization of get_calibration
aromanielloNTIA Mar 28, 2024
2eb839f
simplify calibration file example
aromanielloNTIA Mar 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/github-actions-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ jobs:
source ./env.template
export MOCK_SIGAN=1
export MOCK_SIGAN_RANDOM=1
docker-compose build --no-cache
docker-compose up -d
docker compose build --no-cache
docker compose up -d

- name: Wait for containers # wait for containers to finish starting
run: sleep 45
Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,12 +422,11 @@ specific to the sensor you are using.

### Sensor Calibration File

By default, scos-sensor will use `configs/default_calibration.json` as the sensor
calibration file. However, if`configs/sensor_calibration.json` or
`configs/sigan_calibration.json` exist they will be used instead of the default
calibration file. Sensor calibration files allow scos-sensor to apply a gain based
on a laboratory calibration of the sensor and may also contain other useful
metadata that characterizes the sensor performance. For additional
By default, scos-sensor will use `configs/default_sensor_calibration.json` as the sensor
jhazentia marked this conversation as resolved.
Show resolved Hide resolved
calibration file. However, if `configs/sensor_calibration.json` exists it will be used
instead of the default calibration file. Sensor calibration files allow SCOS Sensor to
jhazentia marked this conversation as resolved.
Show resolved Hide resolved
scale data based on a laboratory or in-field calibration of the sensor and may also
contain other useful metadata that characterizes the sensor performance. For additional
information on the calibration data, see the
[NTIA-Sensor SigMF Calibration Object](https://github.com/NTIA/sigmf-ns-ntia/blob/master/ntia-sensor.sigmf-ext.md#08-the-calibration-object).
The default calibration file is shown below:
Expand Down
5 changes: 3 additions & 2 deletions docker-compose.yml → compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: '3'

services:
db:
image: postgres:15-alpine
Expand Down Expand Up @@ -45,6 +43,7 @@ services:
- ADDITIONAL_USER_NAMES
- ADDITIONAL_USER_PASSWORD
- AUTHENTICATION
- CALIBRATION_EXPIRATION_LIMIT
- CALLBACK_AUTHENTICATION
- CALLBACK_SSL_VERIFICATION
- CALLBACK_TIMEOUT
Expand All @@ -70,6 +69,8 @@ services:
- SIGAN_CLASS
- SIGAN_POWER_SWITCH
- SIGAN_POWER_CYCLE_STATES
- STARTUP_CALIBRATION_ACTION
- RAY_INIT
- RUNNING_MIGRATIONS
- USB_DEVICE
expose:
Expand Down
16 changes: 0 additions & 16 deletions configs/default_calibration.json

This file was deleted.

17 changes: 11 additions & 6 deletions env.template
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ ADMIN_PASSWORD=password

# set to CERT to enable scos-sensor certificate authentication
AUTHENTICATION=TOKEN

BASE_IMAGE=ghcr.io/ntia/scos-tekrsa/tekrsa_usb:0.2.3
CALIBRATION_EXPIRATION_LIMIT=360
# Default callback api/results
# Set to CERT for certificate authentication
CALLBACK_AUTHENTICATION=TOKEN
Expand All @@ -34,8 +33,6 @@ CALLBACK_TIMEOUT=2
# Use either true or false
DEBUG=true

DEVICE_MODEL=RSA507A

# Use latest as default for local development
DOCKER_TAG=latest

Expand Down Expand Up @@ -75,8 +72,12 @@ SCOS_SENSOR_GIT_TAG="$(git describe --tags)"
# SECURITY WARNING: generate unique key with `manage.py generate_secret_key`
SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(64))')"

# Signal analyzer selection/setup
SIGAN_CLASS=TekRSASigan
SIGAN_MODULE=scos_tekrsa.hardware.tekrsa_sigan
USB_DEVICE=Tektronix
DEVICE_MODEL=RSA507A
BASE_IMAGE=ghcr.io/ntia/scos-tekrsa/tekrsa_usb:0.2.3
aromanielloNTIA marked this conversation as resolved.
Show resolved Hide resolved

# SECURITY WARNING: You should be using certs from a trusted authority.
# If you don't have any, try letsencrypt or a similar service.
Expand All @@ -86,8 +87,12 @@ SSL_CA_PATH=scos_test_ca.crt
SSL_CERT_PATH=sensor01.pem
SSL_KEY_PATH=sensor01.pem

USB_DEVICE=Tektronix

# Calibration action selection
# The action specified here will be used to attempt an onboard
# sensor calibration on startup, if no onboard calibration data
# is available on startup. The specified action must be available.
CALIBRATE_ON_STARTUP=true
aromanielloNTIA marked this conversation as resolved.
Show resolved Hide resolved
STARTUP_CALIBRATION_ACTION=SEA_CBRS_Calibrate_Baseline

# Debug dependant settings
if $DEBUG; then
Expand Down
1 change: 0 additions & 1 deletion gunicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import sys
from multiprocessing import cpu_count


bind = ":8000"
workers = 1
worker_class = "gthread"
Expand Down
5 changes: 1 addition & 4 deletions src/handlers/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from scos_actions.signals import (
location_action_completed,
measurement_action_completed,
trigger_api_restart
trigger_api_restart,
)

logger = logging.getLogger(__name__)
Expand All @@ -20,7 +20,6 @@ def ready(self):
db_location_deleted,
db_location_updated,
location_action_completed_callback,

)
from handlers.measurement_handler import measurement_action_completed_callback

Expand All @@ -40,5 +39,3 @@ def ready(self):

trigger_api_restart.connect(trigger_api_restart_callback)
logger.debug("trigger_api_restart_callback registered to trigger_api_restart")


205 changes: 170 additions & 35 deletions src/initialization/__init__.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,90 @@
import importlib
import logging
import sys
import types
import time
from os import path
from pathlib import Path
from subprocess import check_output
from typing import Optional, Union

from django.conf import settings
from its_preselector.configuration_exception import ConfigurationException
from its_preselector.controlbyweb_web_relay import ControlByWebWebRelay
from its_preselector.preselector import Preselector
from scos_actions.calibration.differential_calibration import DifferentialCalibration
from scos_actions.calibration.sensor_calibration import SensorCalibration
from scos_actions.hardware.utils import power_cycle_sigan
from scos_actions.utils import load_from_json

from utils.signals import register_component_with_status

from .action_loader import ActionLoader
from .capabilities_loader import CapabilitiesLoader
from .sensor_loader import SensorLoader
from .status_monitor import StatusMonitor
from .utils import get_usb_device_exists, set_container_unhealthy

logger = logging.getLogger(__name__)

status_monitor = StatusMonitor()


def get_usb_device_exists() -> bool:
logger.debug("Checking for USB...")
if not settings.RUNNING_TESTS and settings.USB_DEVICE is not None:
usb_devices = check_output("lsusb").decode(sys.stdout.encoding)
logger.debug("Checking for " + settings.USB_DEVICE)
logger.debug("Found " + usb_devices)
return settings.USB_DEVICE in usb_devices
return True
def load_preselector_from_file(
preselector_module, preselector_class, preselector_config_file: Path
):
if preselector_config_file is None:
return None
else:
try:
preselector_config = load_from_json(preselector_config_file)
return load_preselector(
preselector_config, preselector_module, preselector_class
)
except ConfigurationException:
logger.exception(
f"Unable to create preselector defined in: {preselector_config_file}"
)
return None


def load_preselector(
preselector_config: str,
module: str,
preselector_class_name: str,
sensor_definition: dict,
) -> Preselector:
logger.debug(
f"loading {preselector_class_name} from {module} with config: {preselector_config}"
)
if module is not None and preselector_class_name is not None:
preselector_module = importlib.import_module(module)
preselector_constructor = getattr(preselector_module, preselector_class_name)
preselector_config = load_from_json(preselector_config)
ps = preselector_constructor(sensor_definition, preselector_config)
register_component_with_status.send(ps, component=ps)
else:
ps = None
return ps


def load_switches(switch_dir: Path) -> dict:
logger.debug(f"Loading switches in {switch_dir}")
switch_dict = {}
try:
if switch_dir is not None and switch_dir.is_dir():
for f in switch_dir.iterdir():
file_path = f.resolve()
logger.debug(f"loading switch config {file_path}")
conf = load_from_json(file_path)
try:
switch = ControlByWebWebRelay(conf)
logger.debug(f"Adding {switch.id}")
switch_dict[switch.id] = switch
logger.debug(f"Registering switch status for {switch.name}")
register_component_with_status.send(__name__, component=switch)
except ConfigurationException:
logger.error(f"Unable to configure switch defined in: {file_path}")
except Exception as ex:
logger.error(f"Unable to load switches {ex}")
return switch_dict


def status_registration_handler(sender, **kwargs):
Expand All @@ -42,32 +101,108 @@ def set_container_unhealthy():
Path(settings.SDR_HEALTHCHECK_FILE).touch()


def get_calibration(
cal_file_path: str, cal_type: str
) -> Optional[Union[DifferentialCalibration, SensorCalibration]]:
"""
Load calibration data from file.

:param cal_file_path: Path to the JSON calibration file.
:param cal_type: Calibration type to load: "onboard", "sensor" or "differential"
:return: The ``Calibration`` object, if loaded, or ``None`` if loading failed.
"""
try:
cal = None
if cal_file_path is None or cal_file_path == "":
logger.error("No calibration file specified, reverting to none.")
elif not path.exists(cal_file_path):
logger.error(f"{cal_file_path} does not exist, reverting to none.")
else:
logger.debug(f"Loading calibration file: {cal_file_path}")
# Create calibration object
cal_file_path = Path(cal_file_path)
if cal_type.lower() in ["sensor", "onboard"]:
cal = SensorCalibration.from_json(cal_file_path)
elif cal_type.lower() == "differential":
cal = DifferentialCalibration.from_json(cal_file_path)
else:
logger.error(f"Unknown calibration type: {cal_type}")
raise ValueError
except Exception:
cal = None
logger.exception(
f"Unable to load {cal_type} calibration file, reverting to none"
)
finally:
return cal


try:
sensor_loader = None
register_component_with_status.connect(status_registration_handler)
usb_device_exists = get_usb_device_exists()
if usb_device_exists:
action_loader = ActionLoader()
logger.debug(f"Actions ActionLoader has {len(action_loader.actions)} actions")
capabilities_loader = CapabilitiesLoader()
logger.debug("Calling sensor loader.")
sensor_loader = SensorLoader(capabilities_loader.capabilities)
action_loader = ActionLoader()
logger.debug(f"Actions ActionLoader has {len(action_loader.actions)} actions")
capabilities_loader = CapabilitiesLoader()
switches = load_switches(settings.SWITCH_CONFIGS_DIR)
preselector = load_preselector(
settings.PRESELECTOR_CONFIG,
settings.PRESELECTOR_MODULE,
settings.PRESELECTOR_CLASS,
capabilities_loader.capabilities["sensor"],
)

if get_usb_device_exists():
logger.debug("Initializing Sensor...")
sensor_loader = SensorLoader(
capabilities_loader.capabilities, switches, preselector
)

else:
logger.debug("Power cycling sigan")
try:
power_cycle_sigan(switches)
except Exception as power_cycle_exception:
logger.error(f"Unable to power cycle sigan: {power_cycle_exception}")
set_container_unhealthy()
time.sleep(60)

if not settings.RUNNING_MIGRATIONS:
if (
not settings.RUNNING_MIGRATIONS
and not sensor_loader.sensor.signal_analyzer.healthy()
sensor_loader.sensor.signal_analyzer is None
or not sensor_loader.sensor.signal_analyzer.healthy()
):
try:
power_cycle_sigan(switches)
except Exception as power_cycle_exception:
logger.error(f"Unable to power cycle sigan: {power_cycle_exception}")
set_container_unhealthy()
else:
action_loader = types.SimpleNamespace()
action_loader.actions = {}
capabilities_loader = types.SimpleNamespace()
capabilities_loader.capabilities = {}
sensor_loader = types.SimpleNamespace()
sensor_loader.sensor = types.SimpleNamespace()
sensor_loader.sensor.signal_analyzer = None
sensor_loader.preselector = None
sensor_loader.switches = {}
sensor_loader.capabilities = {}
logger.warning("Usb is not ready. Marking container as unhealthy")
set_container_unhealthy()
except:
logger.exception("Error during initialization")
time.sleep(60)

# Calibration loading
if not settings.RUNNING_TESTS:
# Load the onboard cal file as the sensor calibration, if it exists
onboard_cal = get_calibration(settings.ONBOARD_CALIBRATION_FILE, "onboard")
if onboard_cal is not None:
sensor_loader.sensor.sensor_calibration = onboard_cal
else:
# Otherwise, try using the sensor calibration file
sensor_cal = get_calibration(settings.SENSOR_CALIBRATION_FILE, "sensor")
if sensor_cal is not None:
sensor_loader.sensor.sensor_calibration = sensor_cal

# Now load the differential calibration, if it exists
differential_cal = get_calibration(
settings.DIFFERENTIAL_CALIBRATION_FILE,
"differential",
)
sensor_loader.sensor.differential_calibration = differential_cal

import ray

if settings.RAY_INIT and not ray.is_initialized():
# Dashboard is only enabled if ray[default] is installed
logger.debug("Initializing ray.")
ray.init()
except BaseException as error:
logger.exception(f"Error during initialization: {error}")
set_container_unhealthy()
Loading
Loading