diff --git a/CHANGELOG.md b/CHANGELOG.md index 89a7501..cb471ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Maps for CQ Zones, ITU Zones, ITU Regions, and Canadian prefixes. - Attribution for all maps. - Option to append ` | ?help` to the playing status. +- `?dbconv` command to convert voltage, power, and antenna gain values. ### Changed - ARRL/RAC section maps to include all current ARRL/RAC sections. ### Fixed diff --git a/exts/dbconv.py b/exts/dbconv.py new file mode 100644 index 0000000..b45a49d --- /dev/null +++ b/exts/dbconv.py @@ -0,0 +1,201 @@ +""" +Conversion extension for qrm +--- +Copyright (C) 2020 Abigail Gold, 0x5c + +This file is part of qrm2 and is released under the terms of +the GNU General Public License, version 2. +""" + + +import math +from enum import Enum +from typing import Optional + +import discord.ext.commands as commands + +import common as cmn +from data import options as opt + + +# not sure why but UnitConverter and Unit need to be defined before DbConvCog and convert() +class UnitConverter(commands.Converter): + async def convert(self, ctx: commands.Context, argument: str): + try: + return Unit(argument) + except ValueError as e: + raise commands.BadArgument(message=str(e)) + + +class Unit: + def __init__(self, raw: str): + self.raw: str = raw + self.unit: str + self.type: UnitType + self.is_db: bool + self.mult: int + self._parse() + + def _parse(self): + s = self.raw.lower() + if len(s) > 2 and s[:2] == "db": + self.is_db = True + if s[2:] in units: + u = units[s[2:]] + self.mult = u["mult"] + self.unit = u["log"] + self.type = u["type"] + elif s in units: + self.is_db = False + u = units[s] + self.mult = u["mult"] + self.unit = u["scalar"] + self.type = u["type"] + else: + raise ValueError(f"Invalid unit: {self.raw}") + + def __str__(self): + return self.unit + + +class UnitType(Enum): + voltage = 1 + power = 2 + antenna = 3 + + +class DbConvCog(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.command(name="dbconv", aliases=["dbc"], category=cmn.cat.ref) + async def _db_conv(self, ctx: commands.Context, + value: Optional[float] = None, + unit_from: Optional[UnitConverter] = None, + unit_to: Optional[UnitConverter] = None): + """ + Convert between decibels and scalar values for voltage, power, and antenna gain. + + **Valid Units** + *Voltage:* V, mV, µV, uV, dBV, dBmV, dBµV, dBuV + *Power:* fW, mW, W, kW, dBf, dBm, dBW, dBk + *Antenna Gain:* dBi, dBd, dBq + """ + embed = cmn.embed_factory(ctx) + if value is not None and unit_from is not None and unit_to is not None: + converted = convert(value, unit_from, unit_to) + + embed.title = f"{value:.3g} {unit_from} = {converted:.3g} {unit_to}" + embed.colour = cmn.colours.good + else: + embed.title = "Decibel Quick Reference" + embed.description = ( + "Decibels are a great way to easily represent large quantities that are common in electronics. " + "There are a few main types that are used often in radio: voltage, power, and antenna gain. " + "Here are some commonly-used reference levels for each type:" + ) + v_db_info = ("**dBV** = relative to 1 V\n" + "**dBmV** = relative to 1 mV (1e-3 V)\n" + "**dBµV** = relative to 1 µV (1e-6 V)") + embed.add_field(name="Voltage Decibels", value=v_db_info, inline=False) + p_db_info = ("**dBW** = relative to 1 W\n" + "**dBk** = relative to 1 kW (1e3 W)\n" + "**dBm** = relative to 1 mW (1e-3 W)\n" + "**dBf** = relative to 1 fW (1e-15 W)") + embed.add_field(name="Power Decibels", value=p_db_info, inline=False) + a_db_info = ("**dBi** = relative to a theoretical __i__sotropic radiator in free space " + "(equal radiation in all directions)\n" + "**dBd** = relative to a dipole in free space (0 dBd = 2.15 dBi)\n" + "**dBq** = relative to a quarter-wave antenna in free space (0 dBq = -0.85 dBi)") + embed.add_field(name="Antenna Gain Decibels", value=a_db_info, inline=False) + embed.add_field(name="Use the bot to do the conversions", + value=f"`{opt.display_prefix}dbconv [value] [unit_from] [unit_to]`", + inline=False) + await ctx.send(embed=embed) + + +def setup(bot: commands.Bot): + bot.add_cog(DbConvCog(bot)) + + +def convert(initial: float, unit1: Unit, unit2: Unit): + if unit1.type == unit2.type: + # dB to dB + if unit1.is_db and unit2.is_db: + if unit1.mult == unit2.mult: + return initial + elif unit1.type == UnitType.voltage: + return _calc_volt_db(_calc_volt(initial, unit1.mult), unit2.mult) + elif unit1.type == UnitType.power: + return _calc_power_db(_calc_power(initial, unit1.mult), unit2.mult) + elif unit1.type == UnitType.antenna: + return initial + (unit1.mult - unit2.mult) + # V/W to V/W + elif not unit1.is_db and not unit2.is_db: + if unit1.mult == unit2.mult: + return initial + return initial * unit1.mult / unit2.mult + # dB to V/W + elif unit1.is_db and not unit2.is_db: + if unit1.type == UnitType.voltage: + return _calc_volt(initial, unit1.mult) / unit2.mult + elif unit1.type == UnitType.power: + return _calc_power(initial, unit1.mult) / unit2.mult + # V/W to dB + elif not unit1.is_db and unit2.is_db: + if unit1.type == UnitType.voltage: + return _calc_volt_db(initial * unit1.mult, unit2.mult) + elif unit1.type == UnitType.power: + return _calc_power_db(initial * unit1.mult, unit2.mult) + raise ValueError(f"Can't convert between {unit1} and {unit2}") + + +units = { + # voltage + "uv": {"mult": 1e-6, "scalar": "µV", "log": "dBµV", "type": UnitType.voltage}, + "µv": {"mult": 1e-6, "scalar": "µV", "log": "dBµV", "type": UnitType.voltage}, + "mv": {"mult": 1e-3, "scalar": "mV", "log": "dBmV", "type": UnitType.voltage}, + "v": {"mult": 1, "scalar": "V", "log": "dBV", "type": UnitType.voltage}, + # power + "fw": {"mult": 1e-15, "scalar": "fW", "log": "dBf", "type": UnitType.power}, + "f": {"mult": 1e-15, "scalar": "fW", "log": "dBf", "type": UnitType.power}, + "mw": {"mult": 1e-3, "scalar": "mW", "log": "dBm", "type": UnitType.power}, + "m": {"mult": 1e-3, "scalar": "mW", "log": "dBm", "type": UnitType.power}, + "w": {"mult": 1, "scalar": "W", "log": "dBW", "type": UnitType.power}, + "kw": {"mult": 1e3, "scalar": "kW", "log": "dBk", "type": UnitType.power}, + "k": {"mult": 1e3, "scalar": "kW", "log": "dBk", "type": UnitType.power}, + # antenna + "q": {"mult": -0.85, "scalar": None, "log": "dBq", "type": UnitType.antenna}, + "i": {"mult": 0, "scalar": None, "log": "dBi", "type": UnitType.antenna}, + "d": {"mult": 2.15, "scalar": None, "log": "dBd", "type": UnitType.antenna}, +} + + +def _calc_power_db(p: float, ref: float): + return 10 * math.log10(p / ref) + + +def _calc_power(db: float, ref: float): + return 10 ** (db / 10) * ref + + +def _calc_volt_db(v: float, ref: float): + return 20 * math.log10(v / ref) + + +def _calc_volt(db: float, ref: float): + return 10 ** (db / 20) * ref + + +# testing code +if __name__ == "__main__": + while(True): + try: + ip = input("> ").split() + initial = float(ip[0]) + unit1 = Unit(ip[1]) + unit2 = Unit(ip[2]) + conv = convert(initial, unit1, unit2) + print(f"{initial:.2f} {unit1} = {conv:.2f} {unit2}") + except ValueError as e: + print(e) diff --git a/templates/data/options.py b/templates/data/options.py index a588d55..610c19e 100644 --- a/templates/data/options.py +++ b/templates/data/options.py @@ -30,7 +30,7 @@ debug = False owners_uids = (200102491231092736,) # The extensions to load when running the bot. -exts = ["ae7q", "base", "fun", "grid", "ham", "image", "lookup", "morse", "qrz", "study", "weather"] +exts = ["ae7q", "base", "fun", "grid", "ham", "image", "lookup", "morse", "qrz", "study", "weather", "dbconv"] # Either "time", "random", or "fixed" (first item in statuses) status_mode = "fixed"