├── .gitignore ├── LICENSE.txt ├── README.md ├── cc1101 ├── __init__.py ├── __main__.py ├── config.py ├── errors.py ├── ioctl.py └── patable.py ├── examples └── nexus.py ├── mypy.ini ├── setup.py └── tests └── test_config.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | cc1101_python.egg-info/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cc1101-python 2 | 3 | This project provides an interface to the [CC1101 Linux Driver](https://github.com/28757B2/cc1101-driver) to allow receiving and transmitting packets from Python. 4 | 5 | ## Setup 6 | 7 | pip3 install cc1101-python 8 | 9 | # Command Line 10 | 11 | python3 -m cc1101 {tx,rx,config,reset} 12 | 13 | ## config 14 | Retreive the current configuration from the driver. 15 | 16 | `rx` and `tx` print the human-readable configuration options. 17 | 18 | `rx_raw` and `tx_raw` print the register values of the CC1101 for the current RX and TX configs. 19 | 20 | `dev_raw` prints the current register values of the hardware. 21 | 22 | ## reset 23 | Clear the RX and TX configs and reset the radio hardware. 24 | 25 | ## tx/rx 26 | 27 | Transmits or receives packets. 28 | 29 | ### Common Options 30 | 31 | #### `device` 32 | The path to a `/dev/cc1101.x.x` interface provided by the driver. 33 | 34 | #### `frequency` 35 | The frequency to receive/transmit on. Valid values are 300-348, 387-464 and 779-928 MHz. 36 | 37 | #### `modulation` 38 | The modulation scheme to use. Valid values are OOK, FSK_2, FSK_4, GFSK, MSK. 39 | 40 | #### `baud_rate` 41 | The data rate in kBaud to receive/transmit packets. Valid values are within the range 0.6-500 and depend on modulation: 42 | 43 | | Modulation | Baud Rate | 44 | |------------|-----------| 45 | | OOK / GFSK | 0.6 - 250 | 46 | | 2FSK | 0.6 - 500 | 47 | | 4FSK | 0.6 - 300 | 48 | | MSK | 26 - 500 | 49 | 50 | #### `--sync_word` 51 | The Sync Word to use, specified as a two or four byte hexadecimal value (e.g `0f0f`). If four bytes are used, the upper and lower two bytes must be the same (e.g `0f0f0f0f`) 52 | 53 | In RX, the device searches for the specified sync word to begin reception. Set `0x00` to disable the sync word. 54 | 55 | In TX, the sync word is preprended to each packet. 56 | 57 | #### `--deviation` 58 | When using an FSK modulation, sets the deviation in kHz either side of the provided frequency to use for modulation. 59 | 60 | ### `rx` Options 61 | 62 | #### `packet_length` 63 | The number of bytes the radio will receive once RX is triggered, either via sync word or carrier sense threshold. 64 | 65 | #### `--bandwidth` 66 | Sets the receive bandwidth in kHz. Valid values are 67 | 68 | 58,67,81,101,116,135,162,203,232,270,325,406,464,541,650,812 69 | 70 | #### `--carrier-sense` 71 | Sets the carrier sense threshold in dB required to begin RX. Carrier sense can be set to a relative or an absolute value. When a sync word is provided, RX only begins when the carrier sense is above the threshold and the sync word has been received. 72 | 73 | Not specifying a value disables carrier sense. 74 | 75 | Relative values are `+6`, `+10` and `+14`. These cause the radio to begin RX when the Received Signal Strength Indicator (RSSI) suddenly increases by this value. This is the easiest mode to use for basic RX. 76 | 77 | Absolute values are `-7` to `7` dB. These values cause the radio to begin RX when the RSSI exceeds the absolute value specified by `--magn-target` +/- the carrier-sense value. Using absolute carrier sense will likely require adjusting the `--magn-target`, `--max-lna-gain` and `--max-dvga-gain` experimentally until the required RSSI range is reached. `--out-format rssi` can be used to help find this. See Section 17.4 of the [CC1101 Datasheet](https://www.ti.com/lit/ds/symlink/cc1101.pdf) for examples. 78 | 79 | #### `--magn-target` 80 | Sets the target channel filter amplitude in dB. Valid values are: 81 | 82 | 24, 27, 30, 33, 36, 38, 40, 42 83 | 84 | #### `--max-lna-gain` 85 | Decreases the maximum LNA gain by approximately the specified amount in dB. 86 | 87 | Valid values are: 88 | 89 | 0, 3, 6, 7, 9, 12, 15, 17 90 | 91 | #### `--max-dvga-gain` 92 | Decreases the maximum DVGA gain by approximately the specified amount in dB. 93 | 94 | Valid values are: 95 | 96 | 0, 6, 12, 18 97 | 98 | #### `--block` 99 | Hold the device handle open while receiving. This prevents another process from using or reconfiguring the device, but prevents multiplexing of RX/TX on a single device between two processes. 100 | 101 | #### `--out-format` 102 | Set the output format. 103 | 104 | `info` prints the packet received count, Received Signal Strength Indictator (RSSI) and the hexadecimal representation of each packet as it is received. 105 | 106 | `hex` prints the packet as hexadecimal. 107 | 108 | `bin` outputs the raw packet bytes to stdout. This is useful for piping into other tools. 109 | 110 | `rssi` continually outputs the current value of RSSI. 111 | 112 | ### `tx` Options 113 | 114 | #### `frequency` 115 | Frequency to transmit on. In TX mode, frequencies are by default restricted to 315/433/868/915 MHz +/- 1MHz, which allows specifying TX Power as one of the dBm values listed in [TI DN013](https://www.ti.com/lit/an/swra151a/swra151a.pdf). This checking can be disabled by using the `--raw` flag. 116 | 117 | #### `tx_power` 118 | The power in dBm to use for transmission. Values must match one of the values in the appropriate frequency table of [TI DN013](https://www.ti.com/lit/an/swra151a/swra151a.pdf). 119 | 120 | #### `packet` 121 | A sequence of bytes in hexadecimal form to transmit using the CC1101. 122 | 123 | #### `--raw` 124 | In `--raw` mode, `tx_power` is provided as a single byte in hexadecimal, which will be directly set in the CC1101's `PATABLE`. Any valid frequency value can be used. 125 | 126 | ## RX Example 127 | python3 -m cc1101 rx /dev/cc1101.0.0 433 OOK 1 64 128 | 129 | ## TX Example 130 | python3 -m cc1101 tx /dev/cc1101.0.0 433 OOK 1 1.4 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f 131 | 132 | # Python Library 133 | These examples show how to integrate the CC1101 into Python programs. 134 | 135 | ## Receive 136 | ```python 137 | from time import sleep 138 | from binascii import hexlify 139 | 140 | from cc1101.config import RXConfig, Modulation 141 | from cc1101 import CC1101 142 | 143 | rx_config = RXConfig.new(frequency=434, modulation=Modulation.OOK, baud_rate=1, sync_word=0x0000, packet_length=64) 144 | radio = CC1101("/dev/cc1101.0.0", rx_config, blocking=True) 145 | 146 | while True: 147 | packets = radio.receive() 148 | 149 | for packet in packets: 150 | print(f"Received - {hexlify(packet)}") 151 | 152 | sleep(0.1) 153 | ``` 154 | 155 | 156 | ## Transmit 157 | ```python 158 | from binascii import unhexlify 159 | 160 | from cc1101.config import TXConfig, Modulation 161 | from cc1101 import CC1101 162 | 163 | tx_config = TXConfig.new(frequency=434, modulation=Modulation.OOK, baud_rate=1, tx_power=0.1) 164 | radio = CC1101("/dev/cc1101.0.0") 165 | 166 | radio.transmit(tx_config, unhexlify("0f0f0f0f0f0f0f0f0f0f0f")) 167 | ``` 168 | -------------------------------------------------------------------------------- /cc1101/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 3 | """ 4 | 5 | import os 6 | import struct 7 | import errno 8 | 9 | from typing import List, Optional, Type 10 | from types import TracebackType 11 | from cc1101.config import RXConfig, TXConfig, CONFIG_SIZE 12 | from cc1101 import ioctl 13 | from cc1101.errors import DeviceError, DeviceException 14 | 15 | # CC1101 datasheet Table 31 16 | RSSI_OFFSET = 74 17 | 18 | 19 | class CC1101Handle: 20 | """Class to hold a file handle to a CC1101 device""" 21 | 22 | fh: int 23 | blocking: bool 24 | 25 | def __init__(self, fh: int, blocking: bool = False): 26 | self.fh = fh 27 | self.blocking = blocking 28 | 29 | def __enter__(self) -> int: 30 | return self.fh 31 | 32 | def __exit__( 33 | self, 34 | t: Optional[Type[BaseException]], 35 | value: Optional[BaseException], 36 | traceback: Optional[TracebackType], 37 | ) -> None: 38 | if not self.blocking: 39 | self.close() 40 | 41 | def close(self) -> None: 42 | os.close(self.fh) 43 | 44 | 45 | class CC1101: 46 | """Class to control a CC1101 radio using the Linux driver""" 47 | 48 | VERSION = 4 49 | 50 | dev: str 51 | rx_config: Optional[RXConfig] = None 52 | handle: Optional[CC1101Handle] = None 53 | 54 | def __init__( 55 | self, dev: str, rx_config: Optional[RXConfig] = None, blocking: bool = False 56 | ): 57 | self.dev = dev 58 | 59 | if blocking: 60 | self.handle = CC1101Handle(self._open(), True) 61 | 62 | if rx_config is not None: 63 | self.set_rx_config(rx_config) 64 | 65 | def __del__(self) -> None: 66 | if self.handle is not None: 67 | self.handle.close() 68 | 69 | def _open(self) -> int: 70 | if not os.path.exists(self.dev): 71 | raise OSError(f"{self.dev} does not exist") 72 | 73 | fh = os.open(self.dev, os.O_RDWR) 74 | 75 | version = bytearray(4) 76 | ioctl.read(fh, ioctl.IOCTL.GET_VERSION, version) 77 | (version,) = struct.unpack("I", version) 78 | 79 | if version != self.VERSION: 80 | raise OSError(f"Version mismatch - got {version}, expected {self.VERSION}") 81 | 82 | return fh 83 | 84 | def _get_handle(self) -> CC1101Handle: 85 | 86 | if self.handle is None: 87 | return CC1101Handle(self._open(), False) 88 | else: 89 | return self.handle 90 | 91 | def reset(self) -> None: 92 | """Reset the CC1101 device""" 93 | with self._get_handle() as fh: 94 | ioctl.call(fh, ioctl.IOCTL.RESET) 95 | 96 | def set_tx_config(self, tx_config: TXConfig) -> None: 97 | """Set the device transmit configuration""" 98 | with self._get_handle() as fh: 99 | ioctl.write(fh, ioctl.IOCTL.SET_TX_CONF, tx_config.to_bytes()) 100 | 101 | def set_rx_config(self, rx_config: RXConfig) -> None: 102 | """Set the device receive configuration""" 103 | 104 | # If the new config is the same as the old config 105 | if rx_config == self.rx_config: 106 | 107 | # If not blocking mode 108 | if self.handle is None: 109 | # Get the config from the device 110 | device_config = self.get_rx_config_bytes() 111 | 112 | # If the config on the device differs (i.e it has been reconfigured from under us) 113 | if rx_config.to_bytes() != device_config: 114 | # Reconfigure the device 115 | with self._get_handle() as fh: 116 | ioctl.write( 117 | fh, ioctl.IOCTL.SET_RX_CONF, self.rx_config.to_bytes() 118 | ) 119 | 120 | # Otherwise, update the stored config and reconfigure the device 121 | else: 122 | self.rx_config = rx_config 123 | with self._get_handle() as fh: 124 | ioctl.write(fh, ioctl.IOCTL.SET_RX_CONF, self.rx_config.to_bytes()) 125 | 126 | def transmit(self, tx_config: TXConfig, packet: bytes) -> None: 127 | """Transmit a sequence of bytes using a TX configuration""" 128 | with self._get_handle() as fh: 129 | ioctl.write(fh, ioctl.IOCTL.SET_TX_CONF, tx_config.to_bytes()) 130 | os.write(fh, packet) 131 | 132 | def receive(self) -> List[bytes]: 133 | """Read a sequence of packets from the device's receive buffer""" 134 | 135 | if self.rx_config is not None: 136 | self.set_rx_config(self.rx_config) 137 | packets = [] 138 | 139 | with self._get_handle() as fh: 140 | while True: 141 | try: 142 | packets.append(os.read(fh, self.rx_config.packet_length)) 143 | except OSError as e: 144 | if e.errno == errno.ENOMSG: 145 | return packets 146 | elif e.errno == errno.EMSGSIZE: 147 | raise DeviceException(DeviceError.PACKET_SIZE) 148 | elif e.errno == errno.EFAULT: 149 | raise DeviceException(DeviceError.COPY) 150 | 151 | raise IOError("RX config not set") 152 | 153 | def get_rssi(self) -> float: 154 | """Read the current RSSI value from the device""" 155 | rssi_byte = bytearray(1) 156 | self._ioctl(ioctl.IOCTL.GET_RSSI, rssi_byte) 157 | (rssi_dec,) = struct.unpack("B", rssi_byte) 158 | 159 | # Formula from CC1101 datasheet section 17.3 160 | if rssi_dec >= 128: 161 | rssi_dbm = (int(rssi_dec) - 256) / 2 - RSSI_OFFSET 162 | else: 163 | rssi_dbm = int(rssi_dec) / 2 - RSSI_OFFSET 164 | 165 | return rssi_dbm 166 | 167 | def get_max_packet_size(self) -> int: 168 | """Read the configured maximum packet size from the driver""" 169 | max_packet_size = bytearray(4) 170 | self._ioctl(ioctl.IOCTL.GET_MAX_PACKET_SIZE, max_packet_size) 171 | (max_packet_size,) = struct.unpack("I", max_packet_size) 172 | 173 | return int(max_packet_size) 174 | 175 | def _ioctl(self, command: ioctl.IOCTL, out: bytearray) -> None: 176 | """Helper to read a device config""" 177 | with self._get_handle() as fh: 178 | return ioctl.read(fh, command, out) 179 | 180 | def get_device_config(self) -> bytes: 181 | """Get the current device configuration registers as a sequence of bytes""" 182 | config = bytearray(CONFIG_SIZE) 183 | self._ioctl(ioctl.IOCTL.GET_DEV_RAW_CONF, config) 184 | return bytes(config) 185 | 186 | def get_tx_config_raw(self) -> bytes: 187 | """Get the current configuration registers for TX as a sequence of bytes""" 188 | config = bytearray(CONFIG_SIZE) 189 | self._ioctl(ioctl.IOCTL.GET_TX_RAW_CONF, config) 190 | return bytes(config) 191 | 192 | def get_rx_config_raw(self) -> bytes: 193 | """Get the current configuration registers for RX as a sequence of bytes""" 194 | config = bytearray(CONFIG_SIZE) 195 | self._ioctl(ioctl.IOCTL.GET_RX_RAW_CONF, config) 196 | return bytes(config) 197 | 198 | def get_rx_config_bytes(self) -> bytes: 199 | """Get the current RX configuration as struct bytes""" 200 | config = bytearray(RXConfig.size()) 201 | self._ioctl(ioctl.IOCTL.GET_RX_CONF, config) 202 | return bytes(config) 203 | 204 | def get_rx_config(self) -> Optional[RXConfig]: 205 | """Get the current RX configuration""" 206 | return RXConfig.from_bytes(self.get_rx_config_bytes()) 207 | 208 | def get_tx_config(self) -> Optional[TXConfig]: 209 | """Get the current TX configuration""" 210 | config = bytearray(TXConfig.size()) 211 | self._ioctl(ioctl.IOCTL.GET_TX_CONF, config) 212 | return TXConfig.from_bytes(config) 213 | -------------------------------------------------------------------------------- /cc1101/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 3 | """ 4 | 5 | import argparse 6 | import sys 7 | import time 8 | 9 | from binascii import hexlify, unhexlify 10 | 11 | from . import config, CC1101 12 | 13 | 14 | def tx(args: argparse.Namespace) -> None: 15 | """Handle the tx subcommand""" 16 | 17 | modulation = config.Modulation(args.modulation) 18 | frequency = float(args.frequency) 19 | baud_rate = float(args.baud_rate) 20 | sync_word = int(args.sync_word, 16) 21 | 22 | try: 23 | if args.raw: 24 | tx_config = config.TXConfig.new_raw( 25 | frequency, 26 | modulation, 27 | baud_rate, 28 | int(args.tx_power, 16), 29 | args.deviation, 30 | sync_word, 31 | ) 32 | else: 33 | tx_config = config.TXConfig.new( 34 | frequency, 35 | modulation, 36 | baud_rate, 37 | float(args.tx_power), 38 | args.deviation, 39 | sync_word, 40 | ) 41 | 42 | except ValueError as e: 43 | print(f"Error: {e}") 44 | return 45 | 46 | cc1101 = CC1101(args.device, None, False) 47 | 48 | if args.config_only: 49 | cc1101.set_tx_config(tx_config) 50 | else: 51 | cc1101.transmit(tx_config, unhexlify(args.packet)) 52 | 53 | if args.print_registers: 54 | config.print_raw_config(cc1101.get_device_config()) 55 | 56 | 57 | def rx(args: argparse.Namespace) -> None: 58 | """Handle the rx subcommand""" 59 | 60 | modulation = config.Modulation(args.modulation) 61 | frequency = float(args.frequency) 62 | baud_rate = float(args.baud_rate) 63 | sync_word = int(args.sync_word, 16) 64 | packet_size = int(args.packet_size) 65 | 66 | if args.carrier_sense is None: 67 | carrier_sense_mode = config.CarrierSenseMode.DISABLED 68 | carrier_sense = 0 69 | elif args.carrier_sense == "+6": 70 | carrier_sense_mode = config.CarrierSenseMode.RELATIVE 71 | carrier_sense = 6 72 | elif args.carrier_sense == "+10": 73 | carrier_sense_mode = config.CarrierSenseMode.RELATIVE 74 | carrier_sense = 10 75 | elif args.carrier_sense == "+14": 76 | carrier_sense_mode = config.CarrierSenseMode.RELATIVE 77 | carrier_sense = 14 78 | else: 79 | carrier_sense_mode = config.CarrierSenseMode.ABSOLUTE 80 | carrier_sense = int(args.carrier_sense) 81 | 82 | try: 83 | rx_config = config.RXConfig.new( 84 | frequency, 85 | modulation, 86 | baud_rate, 87 | packet_size, 88 | bandwidth=args.bandwidth, 89 | magn_target=args.magn_target, 90 | max_lna_gain=args.max_lna_gain, 91 | max_dvga_gain=args.max_dvga_gain, 92 | carrier_sense_mode=carrier_sense_mode, 93 | carrier_sense=carrier_sense, 94 | deviation=args.deviation, 95 | sync_word=sync_word, 96 | ) 97 | except ValueError as e: 98 | print(f"Error: {e}") 99 | return 100 | 101 | cc1101 = CC1101(args.device, rx_config, args.block) 102 | 103 | if args.print_registers: 104 | config.print_raw_config(cc1101.get_device_config()) 105 | 106 | if not args.config_only: 107 | count = 1 108 | min_rssi = None 109 | max_rssi = None 110 | 111 | print("Receiving Packets", file=sys.stderr) 112 | while True: 113 | if args.out_format == "rssi": 114 | rssi = cc1101.get_rssi() 115 | 116 | if min_rssi is None or rssi < min_rssi: 117 | min_rssi = rssi 118 | 119 | if max_rssi is None or rssi > max_rssi: 120 | max_rssi = rssi 121 | 122 | output = ( 123 | f"\rCurrent: {rssi} dB / Min: {min_rssi} dB / Max: {max_rssi} dB" 124 | ) 125 | sys.stdout.write("\r" + " " * count) 126 | sys.stdout.write("\r" + output) 127 | count = len(output) 128 | else: 129 | for packet in cc1101.receive(): 130 | if args.out_format in ["hex", "info"]: 131 | packet_hex = hexlify(packet).decode("ascii") 132 | 133 | if args.out_format == "info": 134 | print(f"[{count} - {cc1101.get_rssi()} dB] {packet_hex}") 135 | else: 136 | print(packet_hex) 137 | 138 | else: 139 | sys.stdout.buffer.write(packet) 140 | 141 | count += 1 142 | time.sleep(0.1) 143 | 144 | 145 | def conf(args: argparse.Namespace) -> None: 146 | """Handle the conf subcommand""" 147 | 148 | cc1101 = CC1101(args.device, None) 149 | 150 | if args.conf_type == "rx": 151 | print(cc1101.get_rx_config()) 152 | 153 | elif args.conf_type == "tx": 154 | print(cc1101.get_tx_config()) 155 | 156 | elif args.conf_type == "rx_raw": 157 | config.print_raw_config(cc1101.get_rx_config_raw()) 158 | 159 | elif args.conf_type == "tx_raw": 160 | config.print_raw_config(cc1101.get_tx_config_raw()) 161 | 162 | elif args.conf_type == "dev_raw": 163 | config.print_raw_config(cc1101.get_device_config()) 164 | 165 | print(f"Max Packet Size: {cc1101.get_max_packet_size()}") 166 | 167 | 168 | def reset(args: argparse.Namespace) -> None: 169 | """Handle the reset subcommand""" 170 | 171 | cc1101 = CC1101(args.device, None) 172 | cc1101.reset() 173 | 174 | 175 | def main() -> None: 176 | parser = argparse.ArgumentParser(prog="cc1101") 177 | subparsers = parser.add_subparsers() 178 | 179 | tx_parser = subparsers.add_parser("tx", help="Transmit a Packet") 180 | tx_parser.add_argument("device", help="CC1101 Device") 181 | tx_parser.add_argument("frequency", help="frequency (MHz)") 182 | tx_parser.add_argument( 183 | "modulation", 184 | type=config.Modulation.from_string, 185 | choices=list(config.Modulation), 186 | ) 187 | tx_parser.add_argument("baud_rate", help="baud rate (kBaud)") 188 | tx_parser.add_argument("tx_power", help="transmit power (hex or dBm)") 189 | tx_parser.add_argument("packet", help="packet to transmit (hexadecimal string)") 190 | tx_parser.add_argument( 191 | "--sync_word", help="sync word (2 or 4 bytes hexadecimal)", default="0000" 192 | ) 193 | tx_parser.add_argument( 194 | "--deviation", 195 | type=float, 196 | default=47.607422, 197 | help="frequency deviation for FSK modulations (MHz)", 198 | ) 199 | tx_parser.add_argument( 200 | "--raw", 201 | action="store_true", 202 | help="Allow any frequency and use hex values for TX Power", 203 | ) 204 | tx_parser.add_argument( 205 | "--config-only", 206 | action="store_true", 207 | help="configure the radio, but don't transmit", 208 | ) 209 | tx_parser.add_argument( 210 | "--print-registers", 211 | action="store_true", 212 | help="print raw register values after configuration", 213 | ) 214 | tx_parser.set_defaults(func=tx) 215 | 216 | rx_parser = subparsers.add_parser("rx", help="Receive Packets") 217 | rx_parser.add_argument("device", help="CC1101 Device") 218 | rx_parser.add_argument("frequency", help="frequency (MHz") 219 | rx_parser.add_argument( 220 | "modulation", 221 | type=config.Modulation.from_string, 222 | choices=list(config.Modulation), 223 | ) 224 | rx_parser.add_argument("baud_rate", help="baud rate (kBaud)") 225 | rx_parser.add_argument("packet_size", help="receive packet size (bytes)") 226 | rx_parser.add_argument( 227 | "--sync_word", help="sync word (2 or 4 bytes hexadecimal)", default="0" 228 | ) 229 | rx_parser.add_argument( 230 | "--deviation", 231 | type=float, 232 | default=47.607422, 233 | help="frequency deviation for FSK modulations (MHz)", 234 | ) 235 | rx_parser.add_argument( 236 | "--bandwidth", 237 | type=int, 238 | choices=sorted( 239 | [ 240 | int(config.RXConfig.config_to_bandwidth(m, e)) 241 | for m in reversed(range(0, 4)) 242 | for e in reversed(range(0, 4)) 243 | ] 244 | ), 245 | default=203, 246 | help="recieve bandwidth (kHz)", 247 | ) 248 | rx_parser.add_argument( 249 | "--magn-target", 250 | type=int, 251 | choices=[24, 27, 30, 33, 36, 38, 40, 42], 252 | default=33, 253 | help="target channel filter amplitude (dB)", 254 | ) 255 | rx_parser.add_argument( 256 | "--max-lna-gain", 257 | type=int, 258 | choices=[0, 3, 6, 7, 9, 12, 15, 17], 259 | default=0, 260 | help="maximum LNA Gain (-dB)", 261 | ) 262 | rx_parser.add_argument( 263 | "--max-dvga-gain", 264 | type=int, 265 | choices=[0, 6, 12, 18], 266 | default=0, 267 | help="maximum LNA Gain (-dB)", 268 | ) 269 | rx_parser.add_argument( 270 | "--carrier-sense", 271 | choices=["+6", "+10", "+14"] + [str(i) for i in range(-7, 8)], 272 | help="carrier sense threshold (dB). +6, +10 and +14 are relative increases to RSSI. -7 to 7 are absolute values. Disables carrier sense if not set", 273 | ) 274 | rx_parser.add_argument( 275 | "--config-only", 276 | action="store_true", 277 | help="configure the radio, but don't receive", 278 | ) 279 | rx_parser.add_argument( 280 | "--print-registers", 281 | action="store_true", 282 | help="print raw register values after configuration", 283 | ) 284 | rx_parser.add_argument( 285 | "--block", action="store_true", help="obtain an exclusive lock on the device" 286 | ) 287 | rx_parser.add_argument( 288 | "--out-format", 289 | choices=["hex", "bin", "info", "rssi"], 290 | default="hex", 291 | help="output format", 292 | ) 293 | 294 | rx_parser.set_defaults(func=rx) 295 | 296 | conf_parser = subparsers.add_parser("config", help="Get Device Configs") 297 | conf_parser.add_argument("device", help="CC1101 Device") 298 | conf_parser.add_argument( 299 | "conf_type", 300 | help="Config to get", 301 | choices=["rx", "tx", "rx_raw", "tx_raw", "dev_raw"], 302 | ) 303 | conf_parser.set_defaults(func=conf) 304 | 305 | reset_parser = subparsers.add_parser("reset", help="Reset Device") 306 | reset_parser.add_argument("device", help="CC1101 Device") 307 | reset_parser.set_defaults(func=reset) 308 | 309 | args = parser.parse_args() 310 | 311 | if "func" in args: 312 | args.func(args) 313 | else: 314 | parser.print_help() 315 | 316 | 317 | if __name__ == "__main__": 318 | main() 319 | -------------------------------------------------------------------------------- /cc1101/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 3 | """ 4 | 5 | import ctypes 6 | import math 7 | 8 | from enum import IntEnum 9 | from typing import Dict, Tuple, Type, Optional 10 | 11 | from cc1101.errors import ConfigError, ConfigException 12 | 13 | from .patable import TX_POWERS_315, TX_POWERS_433, TX_POWERS_868, TX_POWERS_915 14 | 15 | # Crystal frequency of 26 Mhz 16 | XTAL_FREQ = 26 17 | DEFAULT_DEVIATION = 47.607422 18 | DEFAULT_BANDWIDTH = 203 19 | DEFAULT_SYNC_WORD = 0x0000 20 | DEFAULT_MAX_LNA_GAIN = 0 21 | DEFAULT_MAX_DVGA_GAIN = 0 22 | DEFAULT_MAGN_TARGET = 33 23 | DEFAULT_CARRIER_SENSE = 6 24 | 25 | AVAILABLE_MAX_LNA_GAINS = [0, 3, 6, 7, 9, 12, 15, 17] 26 | AVAILABLE_MAX_DVGA_GAINS = [0, 6, 12, 18] 27 | AVAILABLE_MAGN_TARGETS = [24, 27, 30, 33, 36, 38, 40, 42] 28 | 29 | 30 | class Registers(IntEnum): 31 | """Mapping of register name to address 32 | Extracted from SmartRF studio using @RN@@<<@= 0x@AH@ config string 33 | """ 34 | 35 | IOCFG2 = 0x00 # GDO2 Output Pin Configuration 36 | IOCFG1 = 0x01 # GDO1 Output Pin Configuration 37 | IOCFG0 = 0x02 # GDO0 Output Pin Configuration 38 | FIFOTHR = 0x03 # RX FIFO and TX FIFO Thresholds 39 | SYNC1 = 0x04 # Sync Word, High Byte 40 | SYNC0 = 0x05 # Sync Word, Low Byte 41 | PKTLEN = 0x06 # Packet Length 42 | PKTCTRL1 = 0x07 # Packet Automation Control 43 | PKTCTRL0 = 0x08 # Packet Automation Control 44 | ADDR = 0x09 # Device Address 45 | CHANNR = 0x0A # Channel Number 46 | FSCTRL1 = 0x0B # Frequency Synthesizer Control 47 | FSCTRL0 = 0x0C # Frequency Synthesizer Control 48 | FREQ2 = 0x0D # Frequency Control Word, High Byte 49 | FREQ1 = 0x0E # Frequency Control Word, Middle Byte 50 | FREQ0 = 0x0F # Frequency Control Word, Low Byte 51 | MDMCFG4 = 0x10 # Modem Configuration 52 | MDMCFG3 = 0x11 # Modem Configuration 53 | MDMCFG2 = 0x12 # Modem Configuration 54 | MDMCFG1 = 0x13 # Modem Configuration 55 | MDMCFG0 = 0x14 # Modem Configuration 56 | DEVIATN = 0x15 # Modem Deviation Setting 57 | MCSM2 = 0x16 # Main Radio Control State Machine Configuration 58 | MCSM1 = 0x17 # Main Radio Control State Machine Configuration 59 | MCSM0 = 0x18 # Main Radio Control State Machine Configuration 60 | FOCCFG = 0x19 # Frequency Offset Compensation Configuration 61 | BSCFG = 0x1A # Bit Synchronization Configuration 62 | AGCCTRL2 = 0x1B # AGC Control 63 | AGCCTRL1 = 0x1C # AGC Control 64 | AGCCTRL0 = 0x1D # AGC Control 65 | WOREVT1 = 0x1E # High Byte Event0 Timeout 66 | WOREVT0 = 0x1F # Low Byte Event0 Timeout 67 | WORCTRL = 0x20 # Wake On Radio Control 68 | FREND1 = 0x21 # Front End RX Configuration 69 | FREND0 = 0x22 # Front End TX Configuration 70 | FSCAL3 = 0x23 # Frequency Synthesizer Calibration 71 | FSCAL2 = 0x24 # Frequency Synthesizer Calibration 72 | FSCAL1 = 0x25 # Frequency Synthesizer Calibration 73 | FSCAL0 = 0x26 # Frequency Synthesizer Calibration 74 | RCCTRL1 = 0x27 # RC Oscillator Configuration 75 | RCCTRL0 = 0x28 # RC Oscillator Configuration 76 | FSTEST = 0x29 # Frequency Synthesizer Calibration Control 77 | PTEST = 0x2A # Production Test 78 | AGCTEST = 0x2B # AGC Test 79 | TEST2 = 0x2C # Various Test Settings 80 | TEST1 = 0x2D # Various Test Settings 81 | TEST0 = 0x2E # Various Test Settings 82 | 83 | 84 | CONFIG_SIZE = 0x2F 85 | 86 | 87 | class cc1101_common_config(ctypes.Structure): 88 | """C struct definition for cc1101_common_config from cc1101.h""" 89 | 90 | _fields_ = [ 91 | ("frequency", ctypes.c_uint32), 92 | ("modulation", ctypes.c_uint8), 93 | ("baud_rate_mantissa", ctypes.c_uint8), 94 | ("baud_rate_exponent", ctypes.c_uint8), 95 | ("deviation_mantissa", ctypes.c_uint8), 96 | ("deviation_exponent", ctypes.c_uint8), 97 | ("sync_word", ctypes.c_uint32), 98 | ] 99 | 100 | 101 | class cc1101_rx_config(ctypes.Structure): 102 | """C struct definition for cc1101_rx_config from cc1101.h""" 103 | 104 | _fields_ = [ 105 | ("common", cc1101_common_config), 106 | ("bandwidth_mantissa", ctypes.c_uint8), 107 | ("bandwidth_exponent", ctypes.c_uint8), 108 | ("max_lna_gain", ctypes.c_uint8), 109 | ("max_dvga_gain", ctypes.c_uint8), 110 | ("magn_target", ctypes.c_uint8), 111 | ("carrier_sense_mode", ctypes.c_uint8), 112 | ("carrier_sense", ctypes.c_uint8), 113 | ("packet_length", ctypes.c_uint32), 114 | ] 115 | 116 | 117 | class cc1101_tx_config(ctypes.Structure): 118 | """C struct definition for cc1101_tx_config from cc1101.h""" 119 | 120 | _fields_ = [ 121 | ("common", cc1101_common_config), 122 | ("tx_power", ctypes.c_uint8), 123 | ] 124 | 125 | 126 | class Modulation(IntEnum): 127 | """CC1101 modulation modes""" 128 | 129 | FSK_2 = 0 130 | GFSK = 1 131 | OOK = 3 132 | FSK_4 = 4 133 | MSK = 7 134 | 135 | def __str__(self) -> str: 136 | return self.name 137 | 138 | @classmethod 139 | def from_string(cls: Type["Modulation"], s: str) -> "Modulation": 140 | try: 141 | return cls[s] 142 | except: 143 | raise ValueError() 144 | 145 | 146 | class CarrierSenseMode(IntEnum): 147 | DISABLED = 0 148 | RELATIVE = 1 149 | ABSOLUTE = 2 150 | 151 | 152 | class CommonConfig: 153 | """Class for common configuration properties shared by TX and RX""" 154 | 155 | _frequency: int 156 | _modulation: Modulation 157 | _baud_rate_mantissa: int 158 | _baud_rate_exponent: int 159 | _deviation_mantissa: int 160 | _deviation_exponent: int 161 | _sync_word: int 162 | 163 | def __init__( 164 | self, 165 | frequency: float, 166 | modulation: Modulation, 167 | baud_rate: float, 168 | deviation: float = DEFAULT_DEVIATION, 169 | sync_word: int = DEFAULT_SYNC_WORD, 170 | ): 171 | self.set_frequency(frequency) 172 | self.set_modulation_and_baud_rate(modulation, baud_rate) 173 | self.set_deviation(deviation) 174 | self.set_sync_word(sync_word) 175 | 176 | @staticmethod 177 | def frequency_to_config(frequency: float) -> int: 178 | """Convert a frequency in MHz to a configuration value 179 | 180 | Uses the formula from section 21 of the CC1101 datasheet 181 | """ 182 | 183 | if not ( 184 | (frequency >= 299.999756 and frequency <= 347.999939) 185 | or (frequency >= 386.999939 and frequency <= 463.999786) 186 | or (frequency >= 778.999878 and frequency <= 928.000000) 187 | ): 188 | raise ConfigException(ConfigError.INVALID_FREQUENCY) 189 | multiplier = (frequency * 2**16) / XTAL_FREQ 190 | return int(multiplier) 191 | 192 | @staticmethod 193 | def config_to_frequency(config: int) -> float: 194 | """Convert a configuration value to a frequency in MHz 195 | 196 | Uses the formula from section 21 of the CC1101 datasheet 197 | """ 198 | return round((XTAL_FREQ / 2**16) * config, 6) 199 | 200 | def get_frequency(self) -> float: 201 | """Get the configured frequency""" 202 | return self.config_to_frequency(self._frequency) 203 | 204 | def set_frequency(self, frequency: float) -> None: 205 | """Set the frequency""" 206 | self._frequency = self.frequency_to_config(frequency) 207 | 208 | @staticmethod 209 | def baud_rate_to_config( 210 | modulation: Modulation, baud_rate: float 211 | ) -> Tuple[int, int]: 212 | """Convert a baud rate in kBaud to a configuration value 213 | 214 | Uses the formula from section 12 of the datasheet 215 | 216 | Table 3 of the datasheet specifieds minimum of 0.5 kBaud, maximum of 500 kBaud 217 | """ 218 | 219 | if modulation == Modulation.GFSK or modulation == Modulation.OOK: 220 | if baud_rate < 0.599742 or baud_rate > 249.939: 221 | raise ConfigException(ConfigError.INVALID_BAUD_RATE) 222 | elif modulation == Modulation.FSK_2: 223 | if baud_rate < 0.599742 or baud_rate > 500: 224 | raise ConfigException(ConfigError.INVALID_BAUD_RATE) 225 | elif modulation == Modulation.FSK_4: 226 | if baud_rate < 0.599742 or baud_rate > 299.927: 227 | raise ConfigException(ConfigError.INVALID_BAUD_RATE) 228 | elif modulation == Modulation.MSK: 229 | if baud_rate < 25.9857 or baud_rate > 499.878: 230 | raise ConfigException(ConfigError.INVALID_BAUD_RATE) 231 | 232 | xtal_freq = XTAL_FREQ * 1000000 233 | 234 | r_data = baud_rate * 1000 235 | 236 | exponent = math.floor(math.log((r_data * 2**20) / xtal_freq, 2)) 237 | 238 | mantissa = int( 239 | round( 240 | ((r_data * 2**28) / (xtal_freq * 2**exponent)) - 256, 241 | 0, 242 | ) 243 | ) 244 | 245 | return mantissa, exponent 246 | 247 | @staticmethod 248 | def config_to_baud_rate(mantissa: int, exponent: int) -> float: 249 | """Convert a baud rate configuration value to kBaud""" 250 | xtal_freq = XTAL_FREQ * 1000000 251 | 252 | r_data = (((256 + mantissa) * 2**exponent) / 2**28) * xtal_freq 253 | 254 | baud_rate = float(round(r_data / 1000, 5)) 255 | 256 | return baud_rate 257 | 258 | def get_modulation_and_baud_rate(self) -> Tuple[Modulation, float]: 259 | """Get the configured baud rate""" 260 | return self._modulation, self.config_to_baud_rate( 261 | self._baud_rate_mantissa, self._baud_rate_exponent 262 | ) 263 | 264 | def set_modulation_and_baud_rate( 265 | self, modulation: Modulation, baud_rate: float 266 | ) -> None: 267 | """Set the baud rate""" 268 | self._baud_rate_mantissa, self._baud_rate_exponent = self.baud_rate_to_config( 269 | modulation, baud_rate 270 | ) 271 | self._modulation = modulation 272 | 273 | @staticmethod 274 | def deviation_to_config(deviation: float) -> Tuple[int, int]: 275 | """Convert a deviation in kHz to a configuration value""" 276 | for exponent in range(0, 8): 277 | for mantissa in range(0, 8): 278 | if CommonConfig.config_to_deviation(mantissa, exponent) == deviation: 279 | return mantissa, exponent 280 | 281 | raise ConfigException(ConfigError.INVALID_DEVIATION) 282 | 283 | @staticmethod 284 | def config_to_deviation(mantissa: int, exponent: int) -> float: 285 | """Convert a deviation configuration value to kHz 286 | 287 | Uses the formula from section 16.1 of the datasheet 288 | """ 289 | xtal_freq = XTAL_FREQ * 1000000 290 | 291 | f_dev = float((xtal_freq / 2**17) * (8 + mantissa) * (2**exponent)) 292 | 293 | return round(f_dev / 1000, 6) 294 | 295 | def get_deviation(self) -> float: 296 | """Get the configured deviation""" 297 | return self.config_to_deviation( 298 | self._deviation_mantissa, self._deviation_exponent 299 | ) 300 | 301 | def set_deviation(self, deviation: float) -> None: 302 | """Set deviation""" 303 | self._deviation_mantissa, self._deviation_exponent = self.deviation_to_config( 304 | deviation 305 | ) 306 | 307 | def get_sync_word(self) -> int: 308 | """Get the configured sync word""" 309 | return self._sync_word 310 | 311 | def set_sync_word(self, sync_word: int) -> None: 312 | """Set the sync word 313 | 314 | Any 16-bit sync word between 0x0000 and 0xFFFF is allowed 315 | For a 32-bit sync word, the high and low 16-bits must be the same 316 | """ 317 | if sync_word < 0 or sync_word > 0xFFFFFFFF: 318 | raise ConfigException(ConfigError.INVALID_SYNC_WORD) 319 | 320 | if sync_word > 0xFFFF: 321 | if sync_word & 0x0000FFFF != sync_word >> 16: 322 | raise ConfigException(ConfigError.INVALID_SYNC_WORD) 323 | 324 | self._sync_word = sync_word 325 | 326 | @classmethod 327 | def size(cls: Type["CommonConfig"]) -> int: 328 | """Get the size in bytes of the configuration struct""" 329 | return ctypes.sizeof(cc1101_common_config) 330 | 331 | @classmethod 332 | def from_struct( 333 | cls: Type["CommonConfig"], config: cc1101_common_config 334 | ) -> "CommonConfig": 335 | """Construct a CommonConfig from a cc1101_common_config struct""" 336 | 337 | frequency = cls.config_to_frequency(config.frequency) 338 | 339 | baud_rate = cls.config_to_baud_rate( 340 | config.baud_rate_mantissa, config.baud_rate_exponent 341 | ) 342 | 343 | deviation = cls.config_to_deviation( 344 | config.deviation_mantissa, config.deviation_exponent 345 | ) 346 | 347 | return cls(frequency, config.modulation, baud_rate, deviation, config.sync_word) 348 | 349 | def to_struct(self) -> cc1101_common_config: 350 | """Serialize a CommonConfig to a cc1101_common_config struct""" 351 | 352 | return cc1101_common_config( 353 | self._frequency, 354 | self._modulation, 355 | self._baud_rate_mantissa, 356 | self._baud_rate_exponent, 357 | self._deviation_mantissa, 358 | self._deviation_exponent, 359 | self._sync_word, 360 | ) 361 | 362 | @classmethod 363 | def from_bytes( 364 | cls: Type["CommonConfig"], config_bytes: bytes 365 | ) -> Optional["CommonConfig"]: 366 | """Convert struct bytes from the CC1101 driver to a CommonConfig""" 367 | 368 | print(config_bytes) 369 | 370 | # Check for all zeroes in the config (not configured) 371 | if sum(config_bytes) == 0: 372 | return None 373 | 374 | config = cc1101_common_config.from_buffer_copy(config_bytes) 375 | return cls.from_struct(config) 376 | 377 | def to_bytes(self) -> bytearray: 378 | """Convert configuration to struct bytes to send to the CC1101 driver""" 379 | return bytearray(self.to_struct()) 380 | 381 | def __repr__(self) -> str: 382 | 383 | modulation, baud_rate = self.get_modulation_and_baud_rate() 384 | 385 | ret = f"Frequency: {self.get_frequency()} MHz\n" 386 | ret += f"Modulation: {Modulation(modulation).name}\n" 387 | ret += f"Baud Rate: {baud_rate} kBaud\n" 388 | ret += f"Deviation: {self.get_deviation()} kHz\n" 389 | ret += f"Sync Word: 0x{self.get_sync_word():08X}\n" 390 | return ret 391 | 392 | 393 | class RXConfig: 394 | """Class for configuration properties required for RX""" 395 | 396 | _common_config: CommonConfig 397 | _bandwidth_mantissa: int 398 | _bandwidth_exponent: int 399 | _max_lna_gain: int 400 | _max_dvga_gain: int 401 | _magn_target: int 402 | _carrier_sense: int 403 | packet_length: int 404 | 405 | def __init__( 406 | self, 407 | common_config: CommonConfig, 408 | packet_length: int, 409 | bandwidth: int = DEFAULT_BANDWIDTH, 410 | carrier_sense_mode: CarrierSenseMode = CarrierSenseMode.RELATIVE, 411 | carrier_sense: int = DEFAULT_CARRIER_SENSE, 412 | max_lna_gain: int = DEFAULT_MAX_LNA_GAIN, 413 | max_dvga_gain: int = DEFAULT_MAX_DVGA_GAIN, 414 | magn_target: int = DEFAULT_MAGN_TARGET, 415 | ): 416 | self.set_common_config(common_config) 417 | self.set_bandwidth(bandwidth) 418 | self.set_carrier_sense(carrier_sense_mode, carrier_sense) 419 | self.set_max_lna_gain(max_lna_gain) 420 | self.set_max_dvga_gain(max_dvga_gain) 421 | self.set_magn_target(magn_target) 422 | self.packet_length = packet_length 423 | 424 | @classmethod 425 | def new( 426 | cls: Type["RXConfig"], 427 | frequency: float, 428 | modulation: Modulation, 429 | baud_rate: float, 430 | packet_length: int, 431 | bandwidth: int = DEFAULT_BANDWIDTH, 432 | carrier_sense_mode: CarrierSenseMode = CarrierSenseMode.RELATIVE, 433 | carrier_sense: int = DEFAULT_CARRIER_SENSE, 434 | max_lna_gain: int = DEFAULT_MAX_LNA_GAIN, 435 | max_dvga_gain: int = DEFAULT_MAX_DVGA_GAIN, 436 | magn_target: int = DEFAULT_MAGN_TARGET, 437 | deviation: float = DEFAULT_DEVIATION, 438 | sync_word: int = DEFAULT_SYNC_WORD, 439 | ) -> "RXConfig": 440 | """Construct a RXConfig from all available parameters""" 441 | common_config = CommonConfig( 442 | frequency, modulation, baud_rate, deviation, sync_word 443 | ) 444 | return cls( 445 | common_config, 446 | packet_length, 447 | bandwidth, 448 | carrier_sense_mode, 449 | carrier_sense, 450 | max_lna_gain, 451 | max_dvga_gain, 452 | magn_target, 453 | ) 454 | 455 | def get_common_config(self) -> CommonConfig: 456 | return self._common_config 457 | 458 | def set_common_config(self, common_config: CommonConfig) -> None: 459 | self._common_config = common_config 460 | 461 | @staticmethod 462 | def bandwidth_to_config(bandwidth: int) -> Tuple[int, int]: 463 | """Convert a bandwidth in kHz to a configuration value""" 464 | for mantissa in range(0, 4): 465 | for exponent in range(0, 4): 466 | if bandwidth == RXConfig.config_to_bandwidth(mantissa, exponent): 467 | return mantissa, exponent 468 | 469 | raise ConfigException(ConfigError.INVALID_BANDWIDTH) 470 | 471 | @staticmethod 472 | def config_to_bandwidth(mantissa: int, exponent: int) -> int: 473 | """Convert a bandwidth configuration value to kHz 474 | 475 | Uses the formula from section 13 of the datasheet 476 | """ 477 | xtal_freq = XTAL_FREQ * 1000000 478 | 479 | bw_channel = xtal_freq / (8 * (4 + mantissa) * 2**exponent) 480 | 481 | return int(bw_channel / 1000) 482 | 483 | def get_bandwidth(self) -> int: 484 | """Get the configured bandwidth""" 485 | return self.config_to_bandwidth( 486 | self._bandwidth_mantissa, self._bandwidth_exponent 487 | ) 488 | 489 | def set_bandwidth(self, bandwidth: int) -> None: 490 | """Set bandwidth""" 491 | self._bandwidth_mantissa, self._bandwidth_exponent = self.bandwidth_to_config( 492 | bandwidth 493 | ) 494 | 495 | def get_carrier_sense(self) -> Tuple[CarrierSenseMode, int]: 496 | """Get the configured carrier sense mode and value""" 497 | return self._carrier_sense_mode, self._carrier_sense 498 | 499 | def set_carrier_sense( 500 | self, carrier_sense_mode: CarrierSenseMode, carrier_sense: int 501 | ) -> None: 502 | 503 | if carrier_sense_mode == CarrierSenseMode.RELATIVE: 504 | if carrier_sense in [6, 10, 14]: 505 | self._carrier_sense_mode = carrier_sense_mode 506 | self._carrier_sense = carrier_sense 507 | else: 508 | raise ConfigException(ConfigError.INVALID_CARRIER_SENSE) 509 | elif carrier_sense_mode == CarrierSenseMode.ABSOLUTE: 510 | if carrier_sense >= -7 and carrier_sense <= 7: 511 | self._carrier_sense_mode = carrier_sense_mode 512 | self._carrier_sense = carrier_sense 513 | else: 514 | raise ConfigException(ConfigError.INVALID_CARRIER_SENSE) 515 | elif carrier_sense_mode == CarrierSenseMode.DISABLED: 516 | self._carrier_sense_mode = carrier_sense_mode 517 | self._carrier_sense = 0 518 | else: 519 | raise ConfigException(ConfigError.INVALID_CARRIER_SENSE) 520 | 521 | def get_max_lna_gain(self) -> int: 522 | """Get the configured maximum LNA gain""" 523 | return self._max_lna_gain 524 | 525 | def set_max_lna_gain(self, max_lna_gain: int) -> None: 526 | """Set maximum LNA gain""" 527 | if max_lna_gain in AVAILABLE_MAX_LNA_GAINS: 528 | self._max_lna_gain = max_lna_gain 529 | else: 530 | raise ConfigException(ConfigError.INVALID_MAX_LNA_GAIN) 531 | 532 | def get_max_dvga_gain(self) -> int: 533 | """Get the configured maximum DVGA gain""" 534 | return self._max_dvga_gain 535 | 536 | def set_max_dvga_gain(self, max_dvga_gain: int) -> None: 537 | """Set maximum DVGA gain""" 538 | 539 | if max_dvga_gain in AVAILABLE_MAX_DVGA_GAINS: 540 | self._max_dvga_gain = max_dvga_gain 541 | else: 542 | raise ConfigException(ConfigError.INVALID_MAX_DVGA_GAIN) 543 | 544 | def get_magn_target(self) -> int: 545 | """Get the configured maximum DVGA gain""" 546 | return self._magn_target 547 | 548 | def set_magn_target(self, magn_target: int) -> None: 549 | """Set maximum DVGA gain""" 550 | 551 | if magn_target in AVAILABLE_MAGN_TARGETS: 552 | self._magn_target = magn_target 553 | else: 554 | raise ConfigException(ConfigError.INVALID_MAGN_TARGET) 555 | 556 | @classmethod 557 | def size(cls) -> int: 558 | return ctypes.sizeof(cc1101_rx_config) 559 | 560 | @classmethod 561 | def from_struct(cls: Type["RXConfig"], config: cc1101_rx_config) -> "RXConfig": 562 | """Construct a RXConfig from a cc1101_rx_config struct""" 563 | 564 | bandwidth = cls.config_to_bandwidth( 565 | config.bandwidth_mantissa, config.bandwidth_exponent 566 | ) 567 | 568 | return cls( 569 | CommonConfig.from_struct(config.common), 570 | config.packet_length, 571 | bandwidth, 572 | config.carrier_sense_mode, 573 | config.carrier_sense, 574 | config.max_lna_gain, 575 | config.max_dvga_gain, 576 | config.magn_target, 577 | ) 578 | 579 | @classmethod 580 | def from_bytes(cls: Type["RXConfig"], config_bytes: bytes) -> Optional["RXConfig"]: 581 | """Convert struct bytes from the CC1101 driver to a RXConfig""" 582 | 583 | # Check for all zeroes in the config (not configured) 584 | if sum(config_bytes) == 0: 585 | return None 586 | 587 | config = cc1101_rx_config.from_buffer_copy(config_bytes) 588 | 589 | return cls.from_struct(config) 590 | 591 | def to_struct(self) -> cc1101_rx_config: 592 | """Serialize a RXConfig to a cc1101_rx_config struct""" 593 | 594 | return cc1101_rx_config( 595 | self._common_config.to_struct(), 596 | self._bandwidth_mantissa, 597 | self._bandwidth_exponent, 598 | self._max_lna_gain, 599 | self._max_dvga_gain, 600 | self._magn_target, 601 | self._carrier_sense_mode, 602 | self._carrier_sense, 603 | self.packet_length, 604 | ) 605 | 606 | def to_bytes(self) -> bytearray: 607 | """Serialize a RXConfig to a cc1101_rx_config struct bytes""" 608 | return bytearray(self.to_struct()) 609 | 610 | def __repr__(self) -> str: 611 | ret = self._common_config.__repr__() 612 | ret += f"Bandwidth: {self.get_bandwidth()} kHz\n" 613 | ret += f"Packet Length: {self.packet_length}\n" 614 | ret += f"Max LNA Gain: -{self.get_max_lna_gain()} dB\n" 615 | ret += f"Max DVGA Gain: -{self.get_max_dvga_gain()} dB\n" 616 | ret += f"Target Channel Filter Amplitude: {self.get_magn_target()} dB\n" 617 | 618 | carrier_sense_mode, carrier_sense = self.get_carrier_sense() 619 | 620 | if carrier_sense_mode == CarrierSenseMode.ABSOLUTE: 621 | ret += f"Carrier Sense: {carrier_sense} dB\n" 622 | elif carrier_sense_mode == CarrierSenseMode.RELATIVE: 623 | ret += f"Carrier Sense: +{carrier_sense} dB\n" 624 | else: 625 | ret += "Carrier Sense: Disabled\n" 626 | 627 | return ret 628 | 629 | def __eq__(self, other: object) -> bool: 630 | if isinstance(other, RXConfig): 631 | return self.to_bytes() == other.to_bytes() 632 | 633 | return False 634 | 635 | 636 | class TXConfig: 637 | """Class for configuration properties required for TX""" 638 | 639 | _common_config: CommonConfig 640 | _tx_power: int 641 | 642 | def __init__( 643 | self, 644 | common_config: CommonConfig, 645 | tx_power: int, 646 | ): 647 | self.set_common_config(common_config) 648 | self.set_tx_power_raw(tx_power) 649 | 650 | @classmethod 651 | def new( 652 | cls: Type["TXConfig"], 653 | frequency: float, 654 | modulation: Modulation, 655 | baud_rate: float, 656 | tx_power: float, 657 | deviation: float = DEFAULT_DEVIATION, 658 | sync_word: int = DEFAULT_SYNC_WORD, 659 | ) -> "TXConfig": 660 | """Construct a TXConfig from a frequency within the ISM bands (315/433/868/915 MHz) and a power output in dBm""" 661 | common_config = CommonConfig( 662 | frequency, modulation, baud_rate, deviation, sync_word 663 | ) 664 | tx_power = TXConfig.tx_power_to_config(frequency, tx_power) 665 | return cls(common_config, tx_power) 666 | 667 | @classmethod 668 | def new_raw( 669 | cls: Type["TXConfig"], 670 | frequency: float, 671 | modulation: Modulation, 672 | baud_rate: float, 673 | tx_power: int, 674 | deviation: float = DEFAULT_DEVIATION, 675 | sync_word: int = DEFAULT_SYNC_WORD, 676 | ) -> "TXConfig": 677 | """Construct a TXConfig with a raw PATABLE TX power value""" 678 | common_config = CommonConfig( 679 | frequency, modulation, baud_rate, deviation, sync_word 680 | ) 681 | return cls(common_config, tx_power) 682 | 683 | def get_common_config(self) -> CommonConfig: 684 | return self._common_config 685 | 686 | def set_common_config(self, common_config: CommonConfig) -> None: 687 | self._common_config = common_config 688 | 689 | @staticmethod 690 | def frequency_near(frequency: float, target_frequency: int) -> bool: 691 | """Determine if a frequency is near to another frequency +/- 1MHz""" 692 | return frequency >= target_frequency - 1 and frequency <= target_frequency + 1 693 | 694 | @staticmethod 695 | def get_power_table(frequency: float) -> Dict[int, float]: 696 | if TXConfig.frequency_near(frequency, 315): 697 | return TX_POWERS_315 698 | elif TXConfig.frequency_near(frequency, 433): 699 | return TX_POWERS_433 700 | elif TXConfig.frequency_near(frequency, 868): 701 | return TX_POWERS_868 702 | elif TXConfig.frequency_near(frequency, 915): 703 | return TX_POWERS_915 704 | else: 705 | raise ConfigException(ConfigError.INVALID_FREQUENCY) 706 | 707 | @staticmethod 708 | def config_to_tx_power(frequency: float, tx_power: int) -> float: 709 | 710 | power_table = TXConfig.get_power_table(frequency) 711 | 712 | if tx_power in power_table: 713 | return power_table[tx_power] 714 | else: 715 | raise ConfigException(ConfigError.INVALID_TX_POWER) 716 | 717 | @staticmethod 718 | def tx_power_to_config(frequency: float, tx_power: float) -> int: 719 | 720 | reversed_power_table = { 721 | v: k for k, v in TXConfig.get_power_table(frequency).items() 722 | } 723 | 724 | if tx_power in reversed_power_table: 725 | return reversed_power_table[tx_power] 726 | else: 727 | raise ConfigException(ConfigError.INVALID_TX_POWER) 728 | 729 | def get_tx_power_raw(self) -> int: 730 | """Get the TX power as a raw PATABLE value""" 731 | return self._tx_power 732 | 733 | def set_tx_power_raw(self, tx_power: int) -> None: 734 | """Set the TX power as a raw PATABLE value""" 735 | if tx_power < 0x00 or tx_power > 0xFF: 736 | raise ConfigException(ConfigError.INVALID_TX_POWER) 737 | self._tx_power = tx_power 738 | 739 | def get_tx_power(self) -> float: 740 | """Get the TX power in dBm 741 | 742 | Configured frequency must be within 1MHz of 315/433/868/915Mhz 743 | """ 744 | return self.config_to_tx_power( 745 | self._common_config.get_frequency(), self._tx_power 746 | ) 747 | 748 | def set_tx_power(self, tx_power: float) -> None: 749 | """Set the TX power in dBm 750 | 751 | Configured frequency must be within 1MHz of 315/433/868/915Mhz 752 | """ 753 | self._tx_power = self.tx_power_to_config( 754 | self._common_config.get_frequency(), tx_power 755 | ) 756 | 757 | @classmethod 758 | def size(cls: Type["TXConfig"]) -> int: 759 | """Get the size in bytes of the configuration struct""" 760 | return ctypes.sizeof(cc1101_tx_config) 761 | 762 | @classmethod 763 | def from_struct(cls: Type["TXConfig"], config: cc1101_tx_config) -> "TXConfig": 764 | """Convert a cc1101_tx_config struct to a TXConfig""" 765 | 766 | return cls(CommonConfig.from_struct(config.common), config.tx_power) 767 | 768 | @classmethod 769 | def from_bytes(cls: Type["TXConfig"], config_bytes: bytes) -> Optional["TXConfig"]: 770 | """Convert struct bytes from the CC1101 driver to a TXConfig""" 771 | 772 | # Check for all zeroes in the config (not configured) 773 | if sum(config_bytes) == 0: 774 | return None 775 | 776 | config = cc1101_tx_config.from_buffer_copy(config_bytes) 777 | return cls.from_struct(config) 778 | 779 | def to_struct(self) -> cc1101_tx_config: 780 | """Serialize a TXConfig to a cc1101_tx_config struct""" 781 | 782 | return cc1101_tx_config(self._common_config.to_struct(), self._tx_power) 783 | 784 | def to_bytes(self) -> bytearray: 785 | """Serialize a TXConfig to cc1101_tx_config struct bytes""" 786 | return bytearray(self.to_struct()) 787 | 788 | def __repr__(self) -> str: 789 | ret = self._common_config.__repr__() 790 | 791 | try: 792 | ret += f"TX Power: {self.get_tx_power()} dBm\n" 793 | except ConfigException: 794 | ret += f"TX Power: 0x{self._tx_power:02X}\n" 795 | 796 | return ret 797 | 798 | 799 | def print_raw_config(config_bytes: bytes) -> None: 800 | """Print an array of CC1101 config bytes as register key/values""" 801 | config = {} 802 | 803 | for r in Registers: 804 | config[r.name] = config_bytes[r.value] 805 | 806 | for k in config.keys(): 807 | print(f"{k}: {config[k]:02x}") 808 | -------------------------------------------------------------------------------- /cc1101/errors.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | 4 | class DeviceError(Enum): 5 | NO_DEVICE = auto() 6 | FILE_HANDLE_CLONE = auto() 7 | INVALID_IOCTL = auto() 8 | VERSION_MISMATCH = auto() 9 | NO_RX_CONFIG = auto() 10 | BUSY = auto() 11 | COPY = auto() 12 | INVALID_CONFIG = auto() 13 | OUT_OF_MEMORY = auto() 14 | BUFFER_EMPTY = auto() 15 | PACKET_SIZE = auto() 16 | UNKNOWN = auto() 17 | 18 | 19 | class ConfigError(Enum): 20 | INVALID_FREQUENCY = auto() 21 | INVALID_BANDWIDTH = auto() 22 | INVALID_CARRIER_SENSE = auto() 23 | INVALID_TX_POWER = auto() 24 | INVALID_BAUD_RATE = auto() 25 | INVALID_DEVIATION = auto() 26 | INVALID_SYNC_WORD = auto() 27 | INVALID_MAX_LNA_GAIN = auto() 28 | INVALID_MAX_DVGA_GAIN = auto() 29 | INVALID_MAGN_TARGET = auto() 30 | 31 | 32 | class CC1101Exception(Exception): 33 | pass 34 | 35 | 36 | class DeviceException(CC1101Exception): 37 | def __init__(self, error: DeviceError): 38 | self.error = error 39 | 40 | 41 | class ConfigException(CC1101Exception): 42 | def __init__(self, error: ConfigError): 43 | self.error = error 44 | -------------------------------------------------------------------------------- /cc1101/ioctl.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 3 | """ 4 | import errno 5 | import fcntl 6 | 7 | from enum import IntEnum 8 | 9 | from cc1101.errors import DeviceError, DeviceException 10 | 11 | DEVICE_CHARACTER = "c" 12 | 13 | 14 | class IOCTL(IntEnum): 15 | """IOCTL values corresponding with those defined in cc1101_chrdev.c in the driver""" 16 | 17 | GET_VERSION = 0 18 | RESET = 1 19 | SET_TX_CONF = 2 20 | SET_RX_CONF = 3 21 | GET_TX_CONF = 4 22 | GET_TX_RAW_CONF = 5 23 | GET_RX_CONF = 6 24 | GET_RX_RAW_CONF = 7 25 | GET_DEV_RAW_CONF = 8 26 | GET_RSSI = 9 27 | GET_MAX_PACKET_SIZE = 10 28 | 29 | 30 | def handle_status(status: int) -> None: 31 | """Convert IOCTL errno to an exception if required""" 32 | 33 | if status == 0: 34 | return 35 | elif status == errno.EIO: 36 | raise DeviceException(DeviceError.INVALID_IOCTL) 37 | elif status == errno.EFAULT: 38 | raise DeviceException(DeviceError.COPY) 39 | elif status == errno.EINVAL: 40 | raise DeviceException(DeviceError.INVALID_CONFIG) 41 | elif status == errno.ENOMEM: 42 | raise DeviceException(DeviceError.OUT_OF_MEMORY) 43 | else: 44 | raise DeviceException(DeviceError.UNKNOWN) 45 | 46 | 47 | def call(fh: int, cmd: IOCTL) -> None: 48 | """Helper for IOCTLs that call driver functions (no arguments)""" 49 | 50 | ioctl = 0 51 | ioctl |= ord(DEVICE_CHARACTER) << 8 52 | ioctl |= cmd 53 | 54 | try: 55 | status = fcntl.ioctl(fh, ioctl) 56 | except OSError as e: 57 | status = e.errno 58 | 59 | handle_status(status) 60 | 61 | 62 | def write(fh: int, cmd: IOCTL, data: bytearray) -> None: 63 | """Helper function for IOCTLs that write data to the driver""" 64 | 65 | ioctl = 0x40000000 66 | ioctl |= len(data) << 16 67 | ioctl |= ord(DEVICE_CHARACTER) << 8 68 | ioctl |= cmd 69 | 70 | try: 71 | status = fcntl.ioctl(fh, ioctl, data, True) 72 | except OSError as e: 73 | status = e.errno 74 | 75 | handle_status(status) 76 | 77 | 78 | def read(fh: int, cmd: IOCTL, data: bytearray) -> None: 79 | """Helper function for IOCTLs that read data from the driver""" 80 | 81 | ioctl = 0x80000000 82 | ioctl |= len(data) << 16 83 | ioctl |= ord(DEVICE_CHARACTER) << 8 84 | ioctl |= cmd 85 | 86 | try: 87 | status = fcntl.ioctl(fh, ioctl, data, True) 88 | except OSError as e: 89 | status = e.errno 90 | 91 | handle_status(status) 92 | -------------------------------------------------------------------------------- /cc1101/patable.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 3 | """ 4 | 5 | # Valid TX powers from Design Note DN013 6 | TX_POWERS_315 = { 7 | 0xC0: 10.6, 8 | 0xC1: 10.3, 9 | 0xC2: 9.9, 10 | 0xC3: 9.6, 11 | 0xC4: 9.2, 12 | 0xC5: 8.8, 13 | 0xC6: 8.5, 14 | 0xC7: 8.2, 15 | 0xC8: 7.9, 16 | 0xC9: 7.5, 17 | 0xCA: 7.2, 18 | 0xCB: 6.9, 19 | 0xCC: 6.6, 20 | # 0x80: 6.6, 21 | 0x81: 6.3, 22 | # 0xCD: 6.3, 23 | 0x82: 6.0, 24 | 0x83: 5.8, 25 | 0xCE: 5.6, 26 | 0x84: 5.4, 27 | 0x85: 5.0, 28 | 0x86: 4.7, 29 | 0x87: 4.3, 30 | 0x88: 3.9, 31 | 0x89: 3.5, 32 | 0x8A: 3.1, 33 | 0xCF: 2.8, 34 | 0x8B: 2.7, 35 | 0x8C: 2.2, 36 | 0x8D: 1.7, 37 | 0x50: 0.7, 38 | 0x8E: 0.6, 39 | 0x60: 0.5, 40 | 0x51: 0.1, 41 | 0x61: -0.1, 42 | 0x40: -0.3, 43 | 0x52: -0.5, 44 | 0x62: -0.7, 45 | 0x3F: -0.8, 46 | 0x3E: -1.0, 47 | 0x53: -1.1, 48 | 0x3D: -1.3, 49 | # 0x63: -1.3, 50 | 0x3C: -1.7, 51 | # 0x54: -1.7, 52 | 0x64: -1.9, 53 | 0x3B: -2.1, 54 | 0x55: -2.3, 55 | 0x65: -2.5, 56 | 0x2F: -2.6, 57 | 0x3A: -2.7, 58 | 0x56: -3.0, 59 | 0x2E: -3.1, 60 | # 0x66: -3.1, 61 | 0x39: -3.4, 62 | 0x57: -3.5, 63 | 0x2D: -3.6, 64 | 0x67: -3.7, 65 | 0x8F: -4.2, 66 | # 0x2C: -4.2, 67 | 0x38: -4.3, 68 | # 0x68: -4.3, 69 | 0x2B: -4.9, 70 | # 0x69: -4.9, 71 | 0x37: -5.4, 72 | 0x6A: -5.5, 73 | 0x2A: -5.7, 74 | 0x6B: -6.1, 75 | 0x29: -6.5, 76 | 0x6C: -6.7, 77 | # 0x36: -6.7, 78 | 0x6D: -7.2, 79 | 0x28: -7.5, 80 | 0x35: -8.1, 81 | 0x6E: -8.4, 82 | 0x27: -8.6, 83 | 0x26: -9.8, 84 | 0x34: -9.9, 85 | 0x25: -11.1, 86 | 0x33: -12.2, 87 | 0x24: -13.0, 88 | 0x6F: -13.2, 89 | 0x1F: -13.3, 90 | 0x1E: -13.9, 91 | 0x1D: -14.5, 92 | 0x1C: -15.2, 93 | 0x23: -15.4, 94 | 0x32: -15.6, 95 | 0x1B: -15.9, 96 | 0x1A: -16.6, 97 | 0x19: -17.5, 98 | 0x18: -18.5, 99 | 0x22: -18.8, 100 | # 0x0F: -18.8, 101 | 0x0E: -19.4, 102 | 0x17: -19.6, 103 | 0x0D: -20.0, 104 | 0x0C: -20.7, 105 | 0x16: -20.9, 106 | 0x31: -21.3, 107 | 0x0B: -21.4, 108 | 0x0A: -22.2, 109 | 0x15: -22.4, 110 | 0x09: -23.0, 111 | 0x08: -24.0, 112 | 0x14: -24.3, 113 | 0x21: -24.5, 114 | 0x07: -25.1, 115 | 0x06: -26.4, 116 | 0x13: -26.6, 117 | 0x05: -27.7, 118 | 0x04: -29.6, 119 | 0x12: -29.8, 120 | 0x03: -31.7, 121 | # 0x02: -34.6, 122 | 0x11: -34.6, 123 | 0x01: -38.3, 124 | 0x10: -41.2, 125 | 0x30: -41.3, 126 | # 0x20: -41.3, 127 | 0x00: -63.8, 128 | } 129 | 130 | TX_POWERS_433 = { 131 | 0xC0: 9.9, 132 | 0xC1: 9.5, 133 | 0xC2: 9.2, 134 | 0xC3: 8.8, 135 | 0xC4: 8.5, 136 | 0xC5: 8.1, 137 | 0xC6: 7.8, 138 | 0xC7: 7.4, 139 | 0xC8: 7.1, 140 | 0xC9: 6.8, 141 | 0xCA: 6.4, 142 | 0x80: 6.3, 143 | 0xCB: 6.1, 144 | 0x81: 6.0, 145 | 0xCC: 5.8, 146 | # 0x82: 5.8, 147 | 0xCD: 5.5, 148 | # 0x83: 5.5, 149 | 0x84: 5.1, 150 | 0xCE: 4.9, 151 | 0x85: 4.8, 152 | 0x86: 4.4, 153 | 0x87: 4.0, 154 | 0x88: 3.6, 155 | 0x89: 3.2, 156 | 0x8A: 2.8, 157 | 0x8B: 2.3, 158 | 0xCF: 2.0, 159 | 0x8C: 1.9, 160 | 0x8D: 1.4, 161 | 0x8E: 0.4, 162 | # 0x50: 0.4, 163 | 0x60: 0.1, 164 | 0x51: -0.3, 165 | 0x61: -0.5, 166 | 0x40: -0.8, 167 | 0x52: -0.9, 168 | 0x62: -1.1, 169 | 0x3E: -1.4, 170 | 0x53: -1.5, 171 | 0x63: -1.7, 172 | 0x3C: -2.1, 173 | 0x54: -2.2, 174 | 0x64: -2.3, 175 | 0x3B: -2.5, 176 | 0x55: -2.8, 177 | 0x65: -2.9, 178 | 0x2F: -3.0, 179 | 0x3A: -3.1, 180 | 0x56: -3.3, 181 | 0x66: -3.5, 182 | 0x39: -3.8, 183 | 0x2D: -4.0, 184 | 0x67: -4.1, 185 | 0x8F: -4.6, 186 | 0x68: -4.7, 187 | 0x69: -5.3, 188 | 0x37: -5.6, 189 | 0x6A: -5.9, 190 | 0x2A: -6.0, 191 | 0x6B: -6.5, 192 | 0x36: -6.8, 193 | 0x29: -6.9, 194 | 0x6C: -7.1, 195 | 0x6D: -7.7, 196 | 0x28: -7.8, 197 | 0x35: -8.3, 198 | 0x27: -8.7, 199 | 0x6E: -8.9, 200 | 0x26: -9.9, 201 | 0x34: -10.1, 202 | 0x25: -11.4, 203 | 0x33: -12.3, 204 | 0x24: -13.3, 205 | 0x1F: -13.7, 206 | 0x1E: -14.3, 207 | 0x1D: -14.9, 208 | 0x1C: -15.5, 209 | 0x23: -15.6, 210 | 0x32: -15.7, 211 | 0x1B: -16.2, 212 | 0x1A: -17.0, 213 | 0x19: -17.8, 214 | 0x18: -18.8, 215 | 0x22: -19.0, 216 | 0x0F: -19.3, 217 | 0x0E: -19.8, 218 | 0x0D: -20.4, 219 | 0x16: -21.0, 220 | 0x31: -21.3, 221 | 0x0B: -21.7, 222 | 0x0A: -22.5, 223 | 0x09: -23.3, 224 | 0x14: -24.3, 225 | 0x21: -24.5, 226 | 0x07: -25.3, 227 | 0x13: -26.5, 228 | 0x05: -27.9, 229 | 0x04: -29.5, 230 | 0x12: -29.6, 231 | 0x03: -31.4, 232 | 0x02: -33.8, 233 | 0x01: -36.5, 234 | 0x20: -38.3, 235 | 0x30: -38.4, 236 | 0x00: -62.7, 237 | } 238 | 239 | TX_POWERS_868 = { 240 | 0xC0: 10.7, 241 | 0xC1: 10.3, 242 | 0xC2: 10.0, 243 | 0xC3: 9.6, 244 | 0xC4: 9.2, 245 | 0xC5: 8.9, 246 | 0xC6: 8.5, 247 | 0xC7: 8.2, 248 | 0xC8: 7.8, 249 | 0xC9: 7.5, 250 | 0xCA: 7.2, 251 | 0xCB: 6.8, 252 | 0xCC: 6.5, 253 | 0xCD: 6.2, 254 | 0xCE: 5.5, 255 | 0x80: 5.2, 256 | 0x81: 5.0, 257 | 0x82: 4.8, 258 | 0x83: 4.6, 259 | 0x84: 4.4, 260 | 0x85: 4.1, 261 | 0x86: 3.7, 262 | 0x87: 3.4, 263 | 0x88: 3.0, 264 | 0x89: 2.6, 265 | 0xCF: 2.4, 266 | 0x8A: 2.1, 267 | 0x8B: 1.7, 268 | 0x8C: 1.1, 269 | 0x8D: 0.6, 270 | 0x50: -0.3, 271 | 0x60: -0.5, 272 | # 0x8E: -0.5, 273 | 0x51: -0.9, 274 | 0x61: -1.1, 275 | 0x40: -1.5, 276 | 0x52: -1.6, 277 | 0x62: -1.8, 278 | 0x53: -2.3, 279 | 0x63: -2.4, 280 | 0x3F: -2.6, 281 | 0x3E: -2.8, 282 | 0x54: -2.9, 283 | 0x64: -3.1, 284 | 0x3D: -3.2, 285 | 0x3C: -3.5, 286 | 0x55: -3.6, 287 | 0x65: -3.7, 288 | 0x3B: -4.0, 289 | 0x56: -4.2, 290 | 0x66: -4.4, 291 | 0x2F: -4.5, 292 | # 0x3A: -4.5, 293 | 0x57: -4.8, 294 | 0x2E: -4.9, 295 | 0x67: -5.0, 296 | 0x39: -5.2, 297 | 0x2D: -5.5, 298 | 0x68: -5.7, 299 | 0x8F: -6.0, 300 | # 0x2C: -6.0, 301 | 0x38: -6.1, 302 | 0x69: -6.3, 303 | 0x2B: -6.7, 304 | 0x6A: -6.9, 305 | # 0x37: -6.9, 306 | 0x2A: -7.4, 307 | 0x6B: -7.5, 308 | 0x36: -8.1, 309 | 0x29: -8.2, 310 | 0x6C: -8.7, 311 | 0x28: -9.0, 312 | 0x35: -9.4, 313 | 0x27: -9.8, 314 | 0x26: -11.0, 315 | 0x34: -11.1, 316 | 0x25: -12.5, 317 | 0x33: -13.3, 318 | 0x24: -14.3, 319 | 0x6D: -14.5, 320 | 0x1F: -14.6, 321 | 0x1E: -15.1, 322 | 0x1D: -15.7, 323 | 0x1C: -16.4, 324 | 0x23: -16.5, 325 | # 0x32: -16.5, 326 | 0x1B: -17.0, 327 | 0x1A: -17.8, 328 | 0x19: -18.6, 329 | 0x18: -19.5, 330 | 0x22: -19.6, 331 | 0x0F: -20.0, 332 | 0x0E: -20.5, 333 | # 0x17: -20.5, 334 | 0x0D: -21.1, 335 | 0x0C: -21.7, 336 | # 0x16: -21.7, 337 | 0x31: -21.9, 338 | 0x0B: -22.3, 339 | 0x0A: -23.0, 340 | # 0x15: -23.0, 341 | 0x09: -23.8, 342 | 0x08: -24.6, 343 | 0x14: -24.7, 344 | 0x21: -24.8, 345 | 0x07: -25.5, 346 | 0x13: -26.5, 347 | # 0x06: -26.5, 348 | 0x05: -27.7, 349 | 0x12: -28.9, 350 | # 0x04: -28.9, 351 | 0x03: -30.2, 352 | 0x02: -31.7, 353 | # 0x11: -31.7, 354 | 0x01: -33.1, 355 | 0x10: -34.1, 356 | # 0x20: -34.1, 357 | 0x30: -34.2, 358 | 0x6E: -45.8, 359 | 0x00: -59.3, 360 | 0x6F: -69.2, 361 | } 362 | 363 | TX_POWERS_915 = { 364 | 0xC0: 9.4, 365 | 0xC1: 9.0, 366 | 0xC2: 8.6, 367 | 0xC3: 8.3, 368 | 0xC4: 7.9, 369 | 0xC5: 7.6, 370 | 0xC6: 7.2, 371 | 0xC7: 6.9, 372 | 0xC8: 6.6, 373 | 0xC9: 6.2, 374 | 0xCA: 5.9, 375 | 0xCB: 5.6, 376 | 0xCC: 5.3, 377 | 0xCD: 5.0, 378 | 0x80: 4.9, 379 | 0x81: 4.7, 380 | 0x82: 4.5, 381 | 0xCE: 4.3, 382 | 0x83: 4.2, 383 | 0x84: 3.9, 384 | 0x85: 3.6, 385 | 0x86: 3.3, 386 | 0x87: 2.9, 387 | 0x88: 2.5, 388 | 0x89: 2.2, 389 | 0x8A: 1.8, 390 | 0xCF: 1.6, 391 | 0x8B: 1.3, 392 | 0x8C: 0.9, 393 | 0x8D: 0.5, 394 | 0x8E: -0.6, 395 | 0x50: -0.9, 396 | 0x60: -1.1, 397 | 0x51: -1.6, 398 | 0x61: -1.8, 399 | 0x40: -2.1, 400 | 0x52: -2.2, 401 | 0x62: -2.4, 402 | 0x3F: -2.5, 403 | 0x3E: -2.7, 404 | 0x53: -2.9, 405 | 0x3D: -3.0, 406 | # 0x63: -3.0, 407 | 0x3C: -3.4, 408 | 0x22: -19.4, 409 | 0x0F: -19.7, 410 | 0x0E: -20.2, 411 | 0x17: -20.3, 412 | 0x0D: -20.8, 413 | 0x0C: -21.4, 414 | # 0x16: -21.4, 415 | 0x31: -21.7, 416 | 0x0B: -22.0, 417 | 0x0A: -22.7, 418 | 0x15: -22.8, 419 | 0x09: -23.5, 420 | 0x6D: -23.8, 421 | 0x08: -24.3, 422 | 0x14: -24.4, 423 | 0x21: -24.6, 424 | 0x07: -25.2, 425 | 0x13: -26.2, 426 | # 0x06: -26.2, 427 | 0x05: -27.3, 428 | 0x12: -28.6, 429 | # 0x04: -28.6, 430 | 0x03: -29.8, 431 | 0x02: -31.2, 432 | 0x11: -31.3, 433 | 0x01: -32.7, 434 | 0x10: -33.6, 435 | 0x20: -33.7, 436 | # 0x30: -33.7, 437 | 0x00: -58.2, 438 | 0x6E: -64.5, 439 | 0x6F: -69.7, 440 | } 441 | -------------------------------------------------------------------------------- /examples/nexus.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2022 3 | 4 | Example showing decoding of data packets from a Nexus protocol weather station using the CC1101 5 | 6 | #### Packet Information (from https://github.com/merbanan/rtl_433/blob/master/src/devices/nexus.c)### 7 | 8 | Nexus sensor protocol with ID, temperature and optional humidity 9 | also FreeTec (Pearl) NC-7345 sensors for FreeTec Weatherstation NC-7344, 10 | also infactory/FreeTec (Pearl) NX-3980 sensors for infactory/FreeTec NX-3974 station, 11 | also Solight TE82S sensors for Solight TE76/TE82/TE83/TE84 stations, 12 | also TFA 30.3209.02 temperature/humidity sensor. 13 | 14 | The sensor sends 36 bits 12 times, 15 | the packets are ppm modulated (distance coding) with a pulse of ~500 us 16 | followed by a short gap of ~1000 us for a 0 bit or a long ~2000 us gap for a 17 | 1 bit, the sync gap is ~4000 us. 18 | 19 | The data is grouped in 9 nibbles: 20 | 21 | [id0] [id1] [flags] [temp0] [temp1] [temp2] [const] [humi0] [humi1] 22 | 23 | - The 8-bit id changes when the battery is changed in the sensor. 24 | - flags are 4 bits B 0 C C, where B is the battery status: 1=OK, 0=LOW 25 | - and CC is the channel: 0=CH1, 1=CH2, 2=CH3 26 | - temp is 12 bit signed scaled by 10 27 | - const is always 1111 (0x0F) 28 | - humidity is 8 bits 29 | """ 30 | 31 | import bitstring 32 | import time 33 | 34 | from cc1101 import CC1101 35 | from cc1101.config import RXConfig, Modulation 36 | 37 | from typing import List 38 | 39 | DEVICE = "/dev/cc1101.0.0" 40 | FREQUENCY = 433.92 41 | PACKET_LENGTH = 1024 42 | 43 | """ 44 | Pulse width is ~500us = 500 * 10^-6 45 | 46 | 2 * (1 / 500 * 10^-6) = 4000 (4kbps) 47 | """ 48 | BAUD_RATE = 4 49 | 50 | def decode_rx_bytes(rx_bytes: bytes) -> List[str]: 51 | """Decode the received bytes to a sequence of Nexus packets (36-bit strings)""" 52 | 53 | # Convert the received bytes to a string of bits 54 | rx_bits = bitstring.BitArray(bytes=rx_bytes).bin 55 | 56 | packets = [] 57 | 58 | bits = "" 59 | count = 0 60 | 61 | # Decode OOK by iterating over each bit 62 | for bit in rx_bits: 63 | # A sequence of 1's seperate each OOK-encoded bit 64 | if bit == "1": 65 | # 10 or more 0's indicates the start of a packet 66 | if count > 10: 67 | # Nexus data packets are 36-bits 68 | if len(bits) == 36: 69 | packets.append(bits) 70 | bits = "" 71 | # 4 or more 0's is an OOK 1 72 | elif count > 4: 73 | bits += "1" 74 | # 1 or more 0's is an OOK 0 75 | elif count > 0: 76 | bits += "0" 77 | count = 0 78 | else: 79 | # Count the number of zeros 80 | count += 1 81 | 82 | return packets 83 | 84 | # Create the RX config and device 85 | rx_config = RXConfig.new(FREQUENCY, Modulation.OOK, BAUD_RATE, PACKET_LENGTH) 86 | radio = CC1101(DEVICE, rx_config) 87 | 88 | # RX Loop 89 | print("Receiving:") 90 | while True: 91 | # Read bytes from the CC1101 92 | for rx_bytes in radio.receive(): 93 | # Decode using OOK 94 | for packet in decode_rx_bytes(rx_bytes): 95 | # Decode the OOK decoded bitstrings based on the packet format 96 | id, battery_ok, const0, channel, temperature, const1, humidity = bitstring.Bits(bin=packet).unpack("uint:8, bool:1, uint:1, uint:2, int:12, uint:4, uint:8") 97 | 98 | # Basic data checks 99 | if channel in [0,1,2] and const0 == 0 and const1 == 0xF and humidity <= 100: 100 | print(f"ID: {id}\nChannel: {channel + 1}\nTemperature: {temperature / 10} °C\nHumidity: {humidity}%\nBattery: {'OK' if battery_ok else 'LOW'}\n") 101 | 102 | time.sleep(1) 103 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | warn_return_any=True 3 | disallow_untyped_defs = True 4 | 5 | [mypy-setuptools] 6 | ignore_missing_imports = True 7 | 8 | [mypy-bitstring] 9 | ignore_missing_imports = True -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r", encoding="utf-8") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="cc1101-python", 8 | version="1.3.1", 9 | author="28757B2", 10 | description="Python interface to the CC1101 Linux device driver", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | url="https://github.com/28757B2/cc1101-python", 14 | packages=setuptools.find_packages(), 15 | python_requires=">=3.6", 16 | classifiers=[ 17 | "Topic :: Home Automation", 18 | "Operating System :: POSIX :: Linux", 19 | "Topic :: System :: Hardware :: Hardware Drivers", 20 | "License :: OSI Approved :: MIT License" 21 | ], 22 | entry_points={ 23 | "console_scripts": { 24 | "cc1101 = cc1101.__main__:main" 25 | } 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from cc1101.config import CommonConfig, Modulation, RXConfig, TXConfig 4 | from cc1101.errors import ConfigException, ConfigError 5 | 6 | VALID_FREQUENCY = 433.92 7 | VALID_MODULATION = Modulation.OOK 8 | VALID_BAUD_RATE = 1 9 | VALID_DEVIATION = 1.586914 10 | VALID_TX_POWER = 9.9 11 | VALID_PACKET_LENGTH = 1024 12 | VALID_SYNC_WORD = 0x00000000 13 | 14 | def test_freq() -> None: 15 | assert CommonConfig.frequency_to_config(315.0) == 0x000C1D89 16 | assert CommonConfig.frequency_to_config(315.0) == 0x000C1D89 17 | assert CommonConfig.frequency_to_config(433.0) == 0x0010A762 18 | assert CommonConfig.frequency_to_config(868.0) == 0x00216276 19 | assert CommonConfig.frequency_to_config(915.0) == 0x0023313B 20 | 21 | assert CommonConfig.frequency_to_config(299.999756) == 0x000B89D8 22 | assert CommonConfig.frequency_to_config(347.999939) == 0x000D6276 23 | assert CommonConfig.frequency_to_config(386.999939) == 0x000EE276 24 | assert CommonConfig.frequency_to_config(463.999786) == 0x0011D89C 25 | assert CommonConfig.frequency_to_config(778.999878) == 0x001DF627 26 | assert CommonConfig.frequency_to_config(928.000000) == 0x0023B13B 27 | 28 | CommonConfig.config_to_frequency(0x000B89D8) == 299.999756 29 | CommonConfig.config_to_frequency(0x000D6276) == 347.999939 30 | CommonConfig.config_to_frequency(0x000EE276) == 386.999939 31 | CommonConfig.config_to_frequency(0x0011D89C) == 463.999786 32 | CommonConfig.config_to_frequency(0x001DF627) == 778.999878 33 | CommonConfig.config_to_frequency(0x0023B13B) == 928.000000 34 | 35 | CommonConfig.config_to_frequency(0x000C1D89) == 314.999664 36 | CommonConfig.config_to_frequency(0x0010A762) == 432.999817 37 | CommonConfig.config_to_frequency(0x00216276) == 867.999939 38 | CommonConfig.config_to_frequency(0x0023313B) == 915.000000 39 | 40 | with pytest.raises(ConfigException) as e_info: 41 | CommonConfig.frequency_to_config(0.0) 42 | assert e_info.value.error == ConfigError.INVALID_FREQUENCY 43 | 44 | with pytest.raises(ConfigException) as e_info: 45 | CommonConfig.frequency_to_config(464.0) 46 | assert e_info.value.error == ConfigError.INVALID_FREQUENCY 47 | 48 | with pytest.raises(ConfigException) as e_info: 49 | CommonConfig.frequency_to_config(999.0) 50 | assert e_info.value.error == ConfigError.INVALID_FREQUENCY 51 | 52 | 53 | def test_baud_rate() -> None: 54 | assert CommonConfig.baud_rate_to_config(Modulation.FSK_2, 0.6) == (0x83, 0x04) 55 | assert CommonConfig.baud_rate_to_config(Modulation.FSK_2, 0.599742) == (0x83, 0x04) 56 | assert CommonConfig.baud_rate_to_config(Modulation.FSK_2, 26.0) == (0x06, 0x0A) 57 | assert CommonConfig.baud_rate_to_config(Modulation.FSK_2, 25.9857) == (0x06, 0x0A) 58 | assert CommonConfig.baud_rate_to_config(Modulation.FSK_2, 250.0) == (0x3B, 0x0D) 59 | assert CommonConfig.baud_rate_to_config(Modulation.FSK_2, 249.939) == (0x3B, 0x0D) 60 | assert CommonConfig.baud_rate_to_config(Modulation.FSK_2, 300.0) == (0x7A, 0x0D) 61 | assert CommonConfig.baud_rate_to_config(Modulation.FSK_2, 299.927) == (0x7A, 0x0D) 62 | assert CommonConfig.baud_rate_to_config(Modulation.FSK_2, 500.0) == (0x3B, 0x0E) 63 | assert CommonConfig.baud_rate_to_config(Modulation.FSK_2, 499.878) == (0x3B, 0x0E) 64 | assert CommonConfig.baud_rate_to_config(Modulation.FSK_2, 115.051) == (0x22, 0x0C) 65 | 66 | assert CommonConfig.config_to_baud_rate(0x83, 0x04) == 0.59974 67 | assert CommonConfig.config_to_baud_rate(0x06, 0x0A) == 25.98572 68 | assert CommonConfig.config_to_baud_rate(0x3B, 0x0D) == 249.93896 69 | assert CommonConfig.config_to_baud_rate(0x7A, 0x0D) == 299.92676 70 | assert CommonConfig.config_to_baud_rate(0x3B, 0x0E) == 499.87793 71 | assert CommonConfig.config_to_baud_rate(0x22, 0x0C) == 115.05127 72 | 73 | with pytest.raises(ConfigException) as e_info: 74 | CommonConfig.baud_rate_to_config(Modulation.FSK_2, 0.0) 75 | assert e_info.value.error == ConfigError.INVALID_BAUD_RATE 76 | 77 | with pytest.raises(ConfigException) as e_info: 78 | CommonConfig.baud_rate_to_config(Modulation.FSK_2, 999.0) 79 | assert e_info.value.error == ConfigError.INVALID_BAUD_RATE 80 | 81 | 82 | def test_deviation() -> None: 83 | CommonConfig.deviation_to_config(1.586914) == (0x00, 0x00) 84 | CommonConfig.deviation_to_config(380.859375) == (0x07, 0x07) 85 | 86 | CommonConfig.config_to_deviation(0x00, 0x00) == 1.586914 87 | CommonConfig.config_to_deviation(0x07, 0x07) == 380.859375 88 | 89 | with pytest.raises(ConfigException) as e_info: 90 | CommonConfig(VALID_FREQUENCY, VALID_MODULATION, VALID_BAUD_RATE, 0.0, VALID_SYNC_WORD) 91 | assert e_info.value.error == ConfigError.INVALID_DEVIATION 92 | 93 | with pytest.raises(ConfigException) as e_info: 94 | CommonConfig(VALID_FREQUENCY, VALID_MODULATION, VALID_BAUD_RATE, 400.0, VALID_SYNC_WORD) 95 | assert e_info.value.error == ConfigError.INVALID_DEVIATION 96 | 97 | def test_sync_word() -> None: 98 | CommonConfig(VALID_FREQUENCY, VALID_MODULATION, VALID_BAUD_RATE, VALID_DEVIATION, 0x00000000) 99 | CommonConfig(VALID_FREQUENCY, VALID_MODULATION, VALID_BAUD_RATE, VALID_DEVIATION, 0x0000FFFF) 100 | CommonConfig(VALID_FREQUENCY, VALID_MODULATION, VALID_BAUD_RATE, VALID_DEVIATION, 0xFFFFFFFF) 101 | 102 | with pytest.raises(ConfigException) as e_info: 103 | CommonConfig(VALID_FREQUENCY, VALID_MODULATION, VALID_BAUD_RATE, VALID_DEVIATION, 0xFFFF0000) 104 | assert e_info.value.error == ConfigError.INVALID_SYNC_WORD 105 | 106 | with pytest.raises(ConfigException) as e_info: 107 | CommonConfig(VALID_FREQUENCY, VALID_MODULATION, VALID_BAUD_RATE, VALID_DEVIATION, 0xAAAABBBB) 108 | assert e_info.value.error == ConfigError.INVALID_SYNC_WORD 109 | 110 | def test_bandwidth() -> None: 111 | assert RXConfig.bandwidth_to_config(812) == (0x00, 0x00) 112 | assert RXConfig.bandwidth_to_config(58) == (0x03, 0x03) 113 | 114 | assert RXConfig.config_to_bandwidth(0x00, 0x00) == 812 115 | assert RXConfig.config_to_bandwidth(0x03, 0x03) == 58 116 | 117 | with pytest.raises(ConfigException) as e_info: 118 | RXConfig.bandwidth_to_config(0) 119 | assert e_info.value.error == ConfigError.INVALID_BANDWIDTH 120 | 121 | with pytest.raises(ConfigException) as e_info: 122 | RXConfig.bandwidth_to_config(400) 123 | assert e_info.value.error == ConfigError.INVALID_BANDWIDTH 124 | 125 | def test_tx_power() -> None: 126 | 127 | for frequency in [315.0, 433.0, 868.0, 915.0]: 128 | power_table = TXConfig.get_power_table(frequency) 129 | for hex, dbm in power_table.items(): 130 | assert TXConfig.config_to_tx_power(frequency, hex) == dbm 131 | assert TXConfig.tx_power_to_config(frequency, dbm) == hex 132 | 133 | 134 | with pytest.raises(ConfigException) as e_info: 135 | TXConfig.config_to_tx_power(123.0, 0xFF) 136 | assert e_info.value.error == ConfigError.INVALID_FREQUENCY 137 | 138 | with pytest.raises(ConfigException) as e_info: 139 | TXConfig.config_to_tx_power(433.0, 0xFF) 140 | assert e_info.value.error == ConfigError.INVALID_TX_POWER 141 | 142 | with pytest.raises(ConfigException) as e_info: 143 | TXConfig.config_to_tx_power(433.0, -1) 144 | assert e_info.value.error == ConfigError.INVALID_TX_POWER 145 | 146 | 147 | def test_to_struct() -> None: 148 | RXConfig.new(VALID_FREQUENCY, VALID_MODULATION, VALID_BAUD_RATE, VALID_PACKET_LENGTH).to_struct() 149 | TXConfig.new(VALID_FREQUENCY, VALID_MODULATION, VALID_BAUD_RATE, VALID_TX_POWER).to_struct() --------------------------------------------------------------------------------