6 Commits f1da47d36b ... 760808ec73

Author SHA1 Message Date
  Fabian Peter Hammerle 760808ec73 representations of PATABLE settings: replace "0x0" with "0" for brevity 3 years ago
  Fabian Peter Hammerle 9a7736aaf6 command cc1101-export-config: append values of PATABLE register as comment 3 years ago
  Fabian Peter Hammerle cebeeb5207 rename set/get_output_power_levels to set/get_output_power as a higher patable setting does not necessarily imply a higher output power level (see table 39) 3 years ago
  Fabian Peter Hammerle 4ae9169f0e examples: set output power levels to invert signals 3 years ago
  Fabian Peter Hammerle b8822f9497 added method set_output_power_levels 3 years ago
  Fabian Peter Hammerle a6d73ebb68 added method get_output_power_levels 3 years ago

+ 5 - 0
CHANGELOG.md

@@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Added
+- method `set/get_output_power` to configure/retrieve output power settings
+  (`PATABLE` and `FREND0.PA_POWER`)
+- command `cc1101-export-config`: append values of `PATABLE` register as comment
+
 ### Fixed
 - default config via private/unstable method `_set_power_amplifier_setting_index`:
   no longer set `FREND0.LODIV_BUF_CURRENT_TX` to `0` (default: `1`)

+ 50 - 4
cc1101/__init__.py

@@ -109,6 +109,16 @@ class _ReceivedPacket:  # unstable
         )
 
 
+def _format_patable(settings: typing.Iterable[int], insert_spaces: bool) -> str:
+    # "Table 39: Optimum PATABLE Settings" uses hexadecimal digits
+    # "0" for brevity
+    settings_hex = tuple(map(lambda s: "0" if s == 0 else "0x{:x}".format(s), settings))
+    if len(settings_hex) == 1:
+        return "({},)".format(settings_hex[0])
+    delimiter = ", " if insert_spaces else ","
+    return "({})".format(delimiter.join(settings_hex))
+
+
 class CC1101:
 
     # pylint: disable=too-many-public-methods
@@ -489,6 +499,7 @@ class CC1101:
         > If OOK modulation is used, the logic 0 and logic 1 power levels
         > shall be programmed to index 0 and 1 respectively.
         """
+        assert 0 <= setting_index <= 0b111, setting_index
         frend0 = self._read_single_byte(ConfigurationRegisterAddress.FREND0)
         frend0 &= 0b11111000
         frend0 |= setting_index
@@ -664,6 +675,9 @@ class CC1101:
                 else "=",
                 self.get_packet_length_bytes(),
             ),
+            "output_power={}".format(
+                _format_patable(self.get_output_power(), insert_spaces=False)
+            ),
         )
         return "CC1101({})".format(", ".join(filter(None, attrs)))
 
@@ -800,10 +814,42 @@ class CC1101:
             )
         )
 
-    def _set_patable(self, setting: typing.Iterable[int]):
-        setting = list(setting)
-        assert 0 < len(setting) <= self._PATABLE_LENGTH_BYTES, setting
-        self._write_burst(start_register=PatableAddress.PATABLE, values=setting)
+    def _set_patable(self, settings: typing.Iterable[int]):
+        settings = list(settings)
+        assert all(0 <= l <= 0xFF for l in settings), settings
+        assert 0 < len(settings) <= self._PATABLE_LENGTH_BYTES, settings
+        self._write_burst(start_register=PatableAddress.PATABLE, values=settings)
+
+    def get_output_power(self) -> typing.Tuple[int, ...]:
+        """
+        Returns the enabled output power settings
+        (up to 8 bytes of the PATABLE register).
+
+        see .set_output_power()
+        """
+        return self._get_patable()[: self._get_power_amplifier_setting_index() + 1]
+
+    def set_output_power(self, power_settings: typing.Iterable[int]) -> None:
+        """
+        Configures output power levels by setting PATABLE and FREND0.PA_POWER.
+        Up to 8 bytes may be provided.
+
+        > [PATABLE] provides flexible PA power ramp up and ramp down
+        > at the start and end of transmission when using 2-FSK, GFSK,
+        > 4-FSK, and MSK modulation as well as ASK modulation shaping.
+
+        For OOK modulation, exactly 2 bytes must be provided:
+        0 to turn off the transmission for logical 0,
+        and a level > 0 to turn on the transmission for logical 1.
+        >>> transceiver.set_output_power((0, 0xC6))
+
+        See "Table 39: Optimum PATABLE Settings for Various Output Power Levels [...]"
+        and section "24 Output Power Programming".
+        """
+        power_settings = list(power_settings)
+        # checks in sub-methods
+        self._set_power_amplifier_setting_index(len(power_settings) - 1)
+        self._set_patable(power_settings)
 
     def _flush_tx_fifo_buffer(self) -> None:
         # > Only issue SFTX in IDLE or TXFIFO_UNDERFLOW states.

+ 6 - 0
cc1101/_cli.py

@@ -111,6 +111,12 @@ def _export_config():
                 )
             )
         print("]")
+        print(
+            "# PATABLE = {}".format(
+                # pylint: disable=protected-access; internal function & method
+                cc1101._format_patable(transceiver._get_patable(), insert_spaces=True)
+            )
+        )
 
 
 def _transmit():

+ 1 - 0
examples/asynchronous_gpio_transmit.py

@@ -15,6 +15,7 @@ GPIO.setup(_GDO0_PIN, GPIO.OUT, initial=GPIO.LOW)
 with cc1101.CC1101() as transceiver:
     transceiver.set_base_frequency_hertz(433.92e6)
     transceiver.set_symbol_rate_baud(600)
+    transceiver.set_output_power((0, 0xC0))  # OOK modulation: (off, on)
     print(transceiver)
     print("starting transmission")
     with transceiver.asynchronous_transmission():

+ 1 - 0
examples/transmit_fixed_length.py

@@ -12,5 +12,6 @@ with cc1101.CC1101() as transceiver:
     transceiver.set_packet_length_mode(cc1101.PacketLengthMode.FIXED)
     transceiver.set_packet_length_bytes(4)
     transceiver.disable_checksum()
+    transceiver.set_output_power((0, 0xC0))  # OOK modulation: (off, on)
     print(transceiver)
     transceiver.transmit(b"\xff\x00\xaa\xff")

+ 2 - 0
examples/transmit_variable_length.py

@@ -14,6 +14,7 @@ with cc1101.CC1101(lock_spi_device=True) as transceiver:
     # transceiver.set_preamble_length_bytes(2)
     # transceiver.set_sync_word(b"\x12\x34")
     # transceiver.disable_checksum()
+    transceiver.set_output_power((0, 0xC0))  # OOK modulation: (off, on)
     print(transceiver)
     print("state", transceiver.get_marc_state().name)
     print("base frequency", transceiver.get_base_frequency_hertz(), "Hz")
@@ -24,6 +25,7 @@ with cc1101.CC1101(lock_spi_device=True) as transceiver:
     if sync_mode != cc1101.SyncMode.NO_PREAMBLE_AND_SYNC_WORD:
         print("preamble length", transceiver.get_preamble_length_bytes(), "bytes")
         print("sync word", transceiver.get_sync_word())
+    print("output power settings (patable)", transceiver.get_output_power())
     print("\nstarting transmission")
     transceiver.transmit(b"\xff\xaa\x00 message")
     time.sleep(1.0)

+ 7 - 0
tests/config/test_0x22_frend0.py

@@ -60,3 +60,10 @@ def test__set_power_amplifier_setting_index(
     ):
         transceiver._set_power_amplifier_setting_index(setting_index)
     transceiver._spi.xfer.assert_called_once_with([0x22 | 0x40, frend0_after])
+
+
+@pytest.mark.parametrize("setting_index", (-1, 8, 21))
+def test__set_power_amplifier_setting_index_invalid(transceiver, setting_index):
+    with pytest.raises(Exception):
+        transceiver._set_power_amplifier_setting_index(setting_index)
+    transceiver._spi.xfer.assert_not_called()

+ 9 - 0
tests/config/test_0x3e_patable.py

@@ -49,3 +49,12 @@ def test__set_patable(transceiver, patable):
     transceiver._spi.xfer.return_value = [0b00000111] * (len(patable) + 1)
     transceiver._set_patable(patable)
     transceiver._spi.xfer.assert_called_once_with([0x3E | 0x40] + list(patable))
+
+
+@pytest.mark.parametrize(
+    "patable", ((21, -7), (0, 42, 256), (0, 1, 2, 3, 4, 5, 6, 7, 8))
+)
+def test__set_patable_invalid(transceiver, patable):
+    with pytest.raises(Exception):
+        transceiver._set_patable(patable)
+    transceiver._spi.xfer.assert_not_called()

+ 6 - 1
tests/test_cli_export_config.py

@@ -60,6 +60,7 @@ def test_export_python_list(capsys, caplog):
             cc1101.addresses.ConfigurationRegisterAddress.IOCFG2: 0x29,
             cc1101.addresses.ConfigurationRegisterAddress.IOCFG1: 0x2E,
         }
+        transceiver_mock().__enter__()._get_patable.return_value = [0xC6] + [0] * 7
         with unittest.mock.patch("sys.argv", [""]):
             with caplog.at_level(logging.INFO):
                 cc1101._cli._export_config()
@@ -68,7 +69,11 @@ def test_export_python_list(capsys, caplog):
     ]
     out, err = capsys.readouterr()
     assert not err
-    assert out == "[\n0b00101001, # 0x29 IOCFG2\n0b00101110, # 0x2e IOCFG1\n]\n"
+    assert (
+        out
+        == "[\n0b00101001, # 0x29 IOCFG2\n0b00101110, # 0x2e IOCFG1\n]\n"
+        + "# PATABLE = (0xc6, 0, 0, 0, 0, 0, 0, 0)\n"
+    )
 
 
 @pytest.mark.parametrize(

+ 47 - 0
tests/test_config.py

@@ -156,3 +156,50 @@ def test_set_base_frequency_hertz_low_warning(transceiver, freq_hz, warn):
         )
     else:
         assert not caught_warnings
+
+
+@pytest.mark.parametrize(
+    ("patable", "patable_index", "power_levels"),
+    (
+        ((198, 0, 0, 0, 0, 0, 0, 0), 0, (198,)),  # CC1101's default
+        ((198, 0, 0, 0, 0, 0, 0, 0), 1, (198, 0)),  # library's default
+        ((0, 198, 0, 0, 0, 0, 0, 0), 1, (0, 198)),
+        ((0, 1, 2, 3, 4, 5, 21, 42), 7, (0, 1, 2, 3, 4, 5, 21, 42)),
+    ),
+)
+def test_get_output_power(transceiver, patable, patable_index, power_levels):
+    with unittest.mock.patch.object(
+        transceiver, "_get_patable", return_value=patable
+    ), unittest.mock.patch.object(
+        transceiver, "_get_power_amplifier_setting_index", return_value=patable_index
+    ):
+        assert transceiver.get_output_power() == power_levels
+
+
+@pytest.mark.parametrize(
+    ("patable_index", "power_levels"),
+    (
+        (0, (198,)),  # CC1101's default
+        (1, (198, 0)),  # library's default
+        (1, (0, 198)),
+        (1, [0, 198]),
+        (7, (0, 1, 2, 3, 4, 5, 21, 42)),
+    ),
+)
+def test_set_output_power(transceiver, patable_index, power_levels):
+    with unittest.mock.patch.object(
+        transceiver, "_set_patable"
+    ) as set_patable_mock, unittest.mock.patch.object(
+        transceiver, "_set_power_amplifier_setting_index"
+    ) as set_patable_index_mock:
+        transceiver.set_output_power(power_levels)
+    set_patable_mock.assert_called_once_with(list(power_levels))
+    set_patable_index_mock.assert_called_once_with(patable_index)
+
+
+@pytest.mark.parametrize(
+    "power_levels", (tuple(), (21, 256), (0, 1, 2, 3, 4, 5, 6, 7, 8))
+)
+def test_set_output_power_invalid(transceiver, power_levels):
+    with pytest.raises(Exception):
+        transceiver.set_output_power(power_levels)