diff --git a/server/app.js b/server/app.js index 3159026..fbf65e7 100644 --- a/server/app.js +++ b/server/app.js @@ -1,10 +1,10 @@ const express = require('express'); const {logInfoMsgPrefix, logWarnMsgPrefix, logErrorMsgPrefix} = require('./utils/utils'); -const packageLock = require('./package-lock.json'); -const { wellsMeasurements } = require('./measurements/payloadMeasurements'); +const packageLock = require('./package-lock.json'); // route imports, see ./routes folder const payloadRouter = require('./routes/payloadRoutes'); +const adcsController = require('./routes/adcsRoutes'); const app = express(); @@ -50,5 +50,6 @@ app.get("/health", (req, res) => { // routes for each endpoint, see ./routes folder app.use('/payload', payloadRouter); +app.use('/adcs', adcsController); module.exports = app; \ No newline at end of file diff --git a/server/controllers/adcsController.js b/server/controllers/adcsController.js index e69de29..4d6d17f 100644 --- a/server/controllers/adcsController.js +++ b/server/controllers/adcsController.js @@ -0,0 +1,184 @@ +const { logInfoMsgPrefix, logWarnMsgPrefix, logErrorMsgPrefix } = require('../utils/utils'); +const angVelocityMeasurements = require('../measurements/adcsMeasurements'); +const magFieldMeasurements = require('../measurements/adcsMeasurements'); +/** + * adcs.js + * @brief This file contains the controller functions for the ADCS API + */ + +const getMagFieldData = async (req, res) => { + console.log(logInfoMsgPrefix('ADCS magnetic field check'), 'request_body:', req.body); + const variant = req.params.variant; + let { start, end } = req.query; // extract query parameters + + // check if variant is provided, shouldn't happen + if (!variant) { + console.log(logWarnMsgPrefix('Variant number not provided')); + res.status(400).json({ error: 'Variant number not provided' }); + return; + } + + // check if variant is valid + if (variant < 1 || variant > 2) { + console.log(logWarnMsgPrefix(`Invalid variant number provided: ${variant}`)); + res.status(400).json({ error: 'Invalid variant number provided, must be 1 or 2' }); + return; + } + + // TODO: should be written into utils/utils.js + // check if start and end are provided, if not, set them to null + // time stamp format is in RFC3339 format, e.g. 2023-10-01T00:00:00Z + if (!start || !end) { + console.log(logWarnMsgPrefix('Start or end time not provided')); + res.status(400).json({ error: 'Start and end time must be provided' }); + return; + } else { + // check if start and end are in valid RFC3339 format + const dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/; // RFC3339 format: YYYY-MM-DDTHH:MM:SSZ + if (!dateRegex.test(start) || !dateRegex.test(end)) { + console.log(logWarnMsgPrefix(`Invalid date format provided: start=${start}, end=${end}`)); + res.status(400).json({ error: 'Invalid date format provided, must be in RFC3339 format' }); + return; + } + + // convert start and end to Date objects to validate + const startDate = new Date(start); + const endDate = new Date(end); + // end date must be after start date + if (endDate <= startDate) { + console.log(logWarnMsgPrefix(`End date must be after start date: start=${start}, end=${end}`)); + res.status(400).json({ error: 'End date must be after start date' }); + return; + } + } + + try { + const queryResult = await magFieldMeasurements.magFieldMeasurements(variant, start, end); + + const merged = {}; + + queryResult.forEach(item => { + const time = item._time; + + if (!merged[time]) { + merged[time] = { + variant: item.variant, + timestamp: time, + LSB: null, + X: null, + Y: null, + Z: null + }; + } + + const axis = item._field.toUpperCase(); + merged[time][axis] = item._value; + + }); + + const finalOutput = Object.values(merged); + console.log(logInfoMsgPrefix('ADCS magnetic field data fetched successfully')); + + res.status(200).json(finalOutput); + + } catch (error) { + console.error(logErrorMsgPrefix('Error fetching ADCS magnetic field data'), error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +const getAngVelocityData = async (req, res) => { + console.log(logInfoMsgPrefix('ADCS angular velocity check'), 'request_body:', req.body); + const variant = req.params.variant; + let { start, end } = req.query; // extract query parameters + + // check if variant is provided, shouldn't happen + if (!variant) { + console.log(logWarnMsgPrefix('Variant number not provided')); + res.status(400).json({ error: 'Variant number not provided' }); + return; + } + + // check if variant is valid + if (variant < 1 || variant > 2) { + console.log(logWarnMsgPrefix(`Invalid variant number provided: ${variant}`)); + res.status(400).json({ error: 'Invalid variant number provided, must be between 1 or 2' }); + return; + } + + // TODO: should be written into utils/utils.js + // check if start and end are provided, if not, set them to null + // time stamp format is in RFC3339 format, e.g. 2023-10-01T00:00:00Z + if (!start || !end) { + console.log(logWarnMsgPrefix('Start or end time not provided')); + res.status(400).json({ error: 'Start and end time must be provided' }); + return; + } else { + // check if start and end are in valid RFC3339 format + const dateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/; // RFC3339 format: YYYY-MM-DDTHH:MM:SSZ + if (!dateRegex.test(start) || !dateRegex.test(end)) { + console.log(logWarnMsgPrefix(`Invalid date format provided: start=${start}, end=${end}`)); + res.status(400).json({ error: 'Invalid date format provided, must be in RFC3339 format' }); + return; + } + + // convert start and end to Date objects to validate + const startDate = new Date(start); + const endDate = new Date(end); + // end date must be after start date + if (endDate <= startDate) { + console.log(logWarnMsgPrefix(`End date must be after start date: start=${start}, end=${end}`)); + res.status(400).json({ error: 'End date must be after start date' }); + return; + } + } + + try { + const queryResult = await angVelocityMeasurements.angVelocityMeasurements(variant, start, end); + + const merged = {}; + + queryResult.forEach(item => { + const time = item._time; + + if (!merged[time]) { + merged[time] = { + variant: item.variant, + timestamp: time, + X: null, + Y: null, + Z: null + }; + } + + const axis = item._field.toUpperCase(); + merged[time][axis] = item._value; + + }); + + const finalOutput = Object.values(merged); + console.log(logInfoMsgPrefix('ADCS angular velocity data fetched successfully')); + + res.status(200).json(finalOutput); + + } catch (error) { + console.error(logErrorMsgPrefix('Error fetching ADCS angular velocity data'), error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +/** + * @brief This function is used to get the measurement of the magnetic field, the data is fetched from the database + * @param variant The variant number + * @query start The start time of the period to get the data + * @query end The end time of the period to get the data + */ +exports.magFieldMeasurements = async (req, res) => getMagFieldData(req, res); + +/** + * @brief This function is used to get the measurement of the angular velocity, the data is fetched from the database + * @param variant The variant number + * @query start The start time of the period to get the data + * @query end The end time of the period to get the data + */ +exports.angVelocityMeasurements = async (req, res) => getAngVelocityData(req, res); diff --git a/server/controllers/payloadController.js b/server/controllers/payloadController.js index 47645c5..cb7cb18 100644 --- a/server/controllers/payloadController.js +++ b/server/controllers/payloadController.js @@ -66,7 +66,7 @@ const getWellData = async (req, res, field) => { timestamp: item._time, [field]: item._value })); - + res.status(200).json(formattedResult); console.log(logInfoMsgPrefix(`Well ${field} data fetched successfully: ${wellNum}`)); } catch (error) { diff --git a/server/docs/api.md b/server/docs/api.md index f68f3cf..2e65460 100644 --- a/server/docs/api.md +++ b/server/docs/api.md @@ -37,3 +37,41 @@ - end - end time of the query range - must in seconds-only RFC3339 date format + +### ADCS + +#### Magnetic Field + +- /adcs/magf/{:variant number}?{start}&{end} + - must have start and end query parameters with seconds-only RFC3339 date format (no fractional seconds) + - will return magnetic field telemetry of the given period + - includes: + - XYZ LSBs (00XXYYZZ) + - X-value MSB + - Y-value MSB + - Z-value MSB + - query parameters + - start + - start time of the query range + - must in seconds-only RFC3339 date format + - end + - end time of the query range + - must in seconds-only RFC3339 date format + +#### Angular Velocity + +- /adcs/angv/{:variant number}?{start}&{end} + - must have start and end query parameters with seconds-only RFC3339 date format (no fractional seconds) + - will return angular velocity telemetry of the given period + - includes: + - X-value + - Y-value + - Z-value + - query parameters + - start + - start time of the query range + - must in seconds-only RFC3339 date format + - end + - end time of the query range + - must in seconds-only RFC3339 date format + \ No newline at end of file diff --git a/server/measurements/adcsMeasurements.js b/server/measurements/adcsMeasurements.js index e69de29..a57803d 100644 --- a/server/measurements/adcsMeasurements.js +++ b/server/measurements/adcsMeasurements.js @@ -0,0 +1,89 @@ +const { queryApi } = require('../db/dbSingleton'); +const { db_bucket } = require('../config/env'); +const { logInfoMsgPrefix, logWarnMsgPrefix, logErrorMsgPrefix } = require('../utils/utils'); + +/** + * adcsMeasurements.js + * @brief This file contains the functions to fetch ADCS related measurements + */ + +// Tag for the ADCS +const adcsTags = 'ADCS'; +// Tag for the magnetic field measurements +const magFieldMeasurementsTag = 'magField'; +// fields for the magnetic field measurements +const magFieldMeasurementsFields = { + LSB: 'LSB', + X: 'X', + Y: 'Y', + Z: 'Z' +}; +// Tag for the angular velocity measurements +const angVelocityMeasurementsTag = 'angVelocity'; +// fields for the angular velocity measurements +const angVelocityMeasurementsFields = { + X: 'X', + Y: 'Y', + Z: 'Z' +}; + +async function magFieldMeasurements(variant_num, start, end) { + const query = `from(bucket: "${db_bucket}") + |> range(start: ${start}, stop: ${end}) + |> filter(fn: (r) => r["_measurement"] == "${magFieldMeasurementsTag}") + |> filter(fn: (r) => r["host"] == "${adcsTags}") + |> filter(fn: (r) => r["variant"] == "${variant_num}") + |> keep(columns: ["_time", "_value", "variant", "_field"]) + `; + return new Promise((resolve, reject) => { + let result = []; + queryApi.queryRows(query, { + next(row, tableMeta) { + const o = tableMeta.toObject(row); + result.push(o); + }, + error(error) { + reject(error); + }, + complete() { + resolve(result); + } + }); + }); +} + +async function angVelocityMeasurements(variant_num, start, end) { + const query = `from(bucket: "${db_bucket}") + |> range(start: ${start}, stop: ${end}) + |> filter(fn: (r) => r["_measurement"] == "${angVelocityMeasurementsTag}") + |> filter(fn: (r) => r["host"] == "${adcsTags}") + |> filter(fn: (r) => r["variant"] == "${variant_num}") + |> keep(columns: ["_time", "_value", "variant", "_field"]) + |> group(columns: ["_time"])`; + + return new Promise((resolve, reject) => { + let result = []; + queryApi.queryRows(query, { + next(row, tableMeta) { + const o = tableMeta.toObject(row); + result.push(o); + }, + error(error) { + reject(error); + }, + complete() { + resolve(result); + } + }) + }) +} + +module.exports = { + magFieldMeasurements, + angVelocityMeasurements, + adcsTags, + magFieldMeasurementsTag, + magFieldMeasurementsFields, + angVelocityMeasurementsTag, + angVelocityMeasurementsFields +}; \ No newline at end of file diff --git a/server/routes/adcsRoutes.js b/server/routes/adcsRoutes.js index e69de29..6ae169b 100644 --- a/server/routes/adcsRoutes.js +++ b/server/routes/adcsRoutes.js @@ -0,0 +1,27 @@ +const express = require('express'); +const adcsController = require('../controllers/adcsController'); + +const router = express.Router(); + +/** + * adcsRoutes.js + * @brief This file contains the routes for the ADCS API + */ + +/** + * @brief This route is used to get the magnetic field data of a variant + * @param variant The variant number + * @query start The start time of the period to get the data + * @query end The end time of the period to get the data + */ +router.route('/magf/:variant').get(adcsController.magFieldMeasurements); + +/** + * @brief This route is used to get the angular velocity data of a variant + * @param variant The variant number + * @query start The start time of the period to get the data + * @query end The end time of the period to get the data + */ +router.route('/angv/:variant').get(adcsController.angVelocityMeasurements); + +module.exports = router; \ No newline at end of file diff --git a/tools/telemetryInputCli/.env b/tools/telemetryInputCli/.env index f800734..11ed37b 100644 --- a/tools/telemetryInputCli/.env +++ b/tools/telemetryInputCli/.env @@ -6,4 +6,7 @@ DB_PASSWORD = "adminPassword" DB_TOKEN = "umsatsAdminToken" PAYLOAD_TAG = "Payload" WELL_TEMP_FIELD = "temp" -WELL_LUMIN_FIELD = "lumin" \ No newline at end of file +WELL_LUMIN_FIELD = "lumin" +ADCS_TAG = "ADCS" +MAG_FIELD = "magfield" +ANG_VEL = "angvel" diff --git a/tools/telemetryInputCli/.gitignore b/tools/telemetryInputCli/.gitignore index 29249e0..d21569e 100644 --- a/tools/telemetryInputCli/.gitignore +++ b/tools/telemetryInputCli/.gitignore @@ -1,4 +1,6 @@ *.csv !examples/data_temp.csv -!examples/data_lumin.csv \ No newline at end of file +!examples/data_lumin.csv +!examples/data_angvel.csv +!examples/data_magField.csv \ No newline at end of file diff --git a/tools/telemetryInputCli/config.yaml b/tools/telemetryInputCli/config.yaml index 422b86e..d0f2ce1 100644 --- a/tools/telemetryInputCli/config.yaml +++ b/tools/telemetryInputCli/config.yaml @@ -9,4 +9,21 @@ tables: # for read the data frame file columns: - timestamp - well_num - - luminosity \ No newline at end of file + - luminosity + + - name: angvel + columns: + - variant + - timestamp + - X + - Y + - Z + + - name: magfield + columns: + - variant + - timestamp + - LSB + - X + - Y + - Z \ No newline at end of file diff --git a/tools/telemetryInputCli/examples/data_angvel.csv b/tools/telemetryInputCli/examples/data_angvel.csv new file mode 100644 index 0000000..a0b9bea --- /dev/null +++ b/tools/telemetryInputCli/examples/data_angvel.csv @@ -0,0 +1,7 @@ +timestamp,variant,X,Y,Z +1756749600,1,3,4,1 +1756749600,2,3,5,6 +1756836000,1,4,7,5 +1756836000,2,7,1,7 +1756922400,1,2,1,6 +1756922400,2,7,5,4 diff --git a/tools/telemetryInputCli/examples/data_magField.csv b/tools/telemetryInputCli/examples/data_magField.csv new file mode 100644 index 0000000..ae76aef --- /dev/null +++ b/tools/telemetryInputCli/examples/data_magField.csv @@ -0,0 +1,7 @@ +timestamp,variant,LSB,X,Y,Z +1756749600,1,256,4,6,7 +1756836000,1,4096,4,7,9 +1756922400,1,65536,5,9,2 +1756749600,2,256,6,8,1 +1756836000,2,4096,4,1,9 +1756922400,2,65536,9,1,2 diff --git a/tools/telemetryInputCli/telemetryInputCli.py b/tools/telemetryInputCli/telemetryInputCli.py index 14426c6..63fb3dc 100644 --- a/tools/telemetryInputCli/telemetryInputCli.py +++ b/tools/telemetryInputCli/telemetryInputCli.py @@ -63,6 +63,15 @@ def insertDataIntoWell(well: int, field: str, data: float, time: datetime, host: point = Point("well").tag("well", well).field(field, data).time(time, WritePrecision.NS).tag("host", host) write_api.write(bucket, org, point) +# insert data into magnetic field +def insertDataIntoMagField(variant: int, lsb: float, x: float, y: float, z: float, time: datetime, host: str, write_api, bucket: str, org: str): + point = Point("magField").tag("variant", variant).field("LSB", lsb).field("X", x).field("Y", y).field("Z", z).time(time, WritePrecision.NS).tag("host", host) + write_api.write(bucket, org, point) + +def insertDataIntoAngVelocity(variant: int, x: float, y: float, z: float, time: datetime, host: str, write_api, bucket: str, org: str): + point = Point("angVelocity").tag("variant", variant).field("X", x).field("Y", y).field("Z", z).time(time, WritePrecision.NS).tag("host", host) + write_api.write(bucket, org, point) + # ask for file path def askingForFilePath() -> str: path = input("Please enter the file path: ") @@ -87,11 +96,14 @@ class TelemetryInputCli(cmd.Cmd): payloadTag = env.get('PAYLOAD_TAG') wellTempField = env.get('WELL_TEMP_FIELD') wellLuminField = env.get('WELL_LUMIN_FIELD') + adcsTag = env.get('ADCS_TAG') + magField = env.get('MAG_FIELD') + angVelocity = env.get('ANG_VEL') client = InfluxDBClient(url=url, token=token, org=org) write_api = client.write_api(write_options=SYNCHRONOUS) prompt = '> ' intro = 'Welcome to the Telemetry Input CLI. Type help or ? to list commands.\n' - + # exit the CLI def do_exit(self, arg): 'Exit the CLI' @@ -134,6 +146,16 @@ def do_bulk_insert(self, arg): if not filePath: return self.insert_data_from_df(table, filePath) + + # insert magnetic field data with specified time + def do_insert_mag_field(self, arg): + 'Insert magnetic field data with specified time' + self.insert_data_into_mag_field(self.magField) + + # insert angular velocity data with specified time + def do_insert_angular_velocity(self, arg): + 'Insert angular velocity data with specified time' + self.insert_data_into_angular_velocity(self.angVelocity) # helper function to insert data into all wells def insert_data_into_wells(self, field): @@ -172,6 +194,7 @@ def insert_data_from_df(self, table:str, filePath:str): else: print("Unsupported file format. Please provide a CSV file.") return + # print out record print(f"Loading {len(df)} records from {filePath} for table {table}.") if df.empty: @@ -183,6 +206,7 @@ def insert_data_from_df(self, table:str, filePath:str): else: print("No timestamp column found in the data.") return + # insert data into influxdb if table == self.wellTempField or table == self.wellLuminField: for index, row in df.iterrows(): @@ -195,11 +219,69 @@ def insert_data_from_df(self, table:str, filePath:str): insertDataIntoWell(well_num, table, data, time, self.payloadTag, self.write_api, self.bucket, self.org) else: print(f"Invalid well number {well_num} at index {index}. Skipping this record.") + + elif table == self.magField: + for index, row in df.iterrows(): + variant = row.get('variant') + if pd.notnull(variant) and (variant == 1 or variant == 2): + time = row.get('timestamp') + if time is not None: + lsb = row.get('LSB') # Figure out what the restriction is for lsb + x = row.get('X') + y = row.get('Y') + z = row.get('Z') + if pd.notnull(lsb): + insertDataIntoMagField(variant, lsb, x, y, z, time, self.adcsTag, self.write_api, self.bucket, self.org) + else: + print(f"Incorrect variant {variant} at index {index}. Skipping this record.") + + elif table == self.angVelocity: + for index, row in df.iterrows(): + variant = row.get('variant') + if pd.notnull(variant) and (variant == 1 or variant == 2): + time = row.get('timestamp') + if time is not None: + x = row.get('X') + y = row.get('Y') + z = row.get('Z') + insertDataIntoAngVelocity(variant, x, y, z, time, self.adcsTag, self.write_api, self.bucket, self.org) + else: + print(f"Incorrect variant {variant} at index {index}. Skipping this record.") + else: - print(f"Unsupported table {table}. Only 'temp' and 'lumin' are supported for bulk insert.") + print(f"Unsupported table {table}. Only 'temp', 'lumin', 'magfield', and 'angvel' are supported for bulk insert.") return print(f"Data from {filePath} inserted into {table} table successfully.") + # helper function to insert magnetic field data + def insert_data_into_mag_field(self, field): + period = askingForPeriod() + calculatedTime = datetime.now(timezone.utc) - parsePeriod(period) + variant = int(input("Please enter the variant number: ")) + while (variant != 1 and variant != 2): + variant = int(input("Please enter the variant number: ")) + lsb = float(input("Please enter the LSB value: ")) + x = float(input("Please enter the X value: ")) + y = float(input("Please enter the Y value: ")) + z = float(input("Please enter the Z value: ")) + + insertDataIntoMagField(variant, lsb, x, y, z, calculatedTime, self.adcsTag, self.write_api, self.bucket, self.org) + print("Magnetic field data inserted successfully") + + # helper function to insert angular velocity data + def insert_data_into_angular_velocity(self, field): + period = askingForPeriod() + calculatedTime = datetime.now(timezone.utc) - parsePeriod(period) + variant = int(input("Please enter the variant number: ")) + while (variant != 1 and variant != 2): + variant = int(input("Please enter the variant number: ")) + x = float(input("Please enter the X value: ")) + y = float(input("Please enter the Y value: ")) + z = float(input("Please enter the Z value: ")) + + insertDataIntoAngVelocity(variant, x, y, z, calculatedTime, self.adcsTag, self.write_api, self.bucket, self.org) + print("Angular Velocity Data inserted successfully") + # helper function to get data from user def get_data_from_user(self): print("Please enter the data you want to insert: ")