diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..6475279 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,16 @@ +name: ci + +on: + pull_request: + push: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Lint + run: | + make fmt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c1b305 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.ruff_cache/ +tools/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3ef409b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-added-large-files + #- id: mixed-line-ending + # args: [ --fix=lf ] + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.6 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff + args: [ --fix, --select, I ] # import sorting + - id: ruff-format + +exclude: 'lib/examples/*' diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e27125f --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +.PHONY: all +all: .venv pre-commit-install help + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development + +.venv: ## Create a virtual environment + @echo "Creating virtual environment..." + @$(MAKE) uv + @$(UV) venv + @$(UV) pip install --requirement pyproject.toml + +.PHONY: pre-commit-install +pre-commit-install: uv + @echo "Installing pre-commit hooks..." + @$(UVX) pre-commit install > /dev/null + +.PHONY: fmt +fmt: pre-commit-install ## Lint and format files + $(UVX) pre-commit run --all-files + +typecheck: .venv ## Run type check + @$(UV) run -m pyright . + +.PHONY: clean +clean: ## Remove all gitignored files + git clean -dfX + +##@ Build Tools +TOOLS_DIR ?= tools +$(TOOLS_DIR): + mkdir -p $(TOOLS_DIR) + +### Tool Versions +UV_VERSION ?= 0.5.24 + +UV_DIR ?= $(TOOLS_DIR)/uv-$(UV_VERSION) +UV ?= $(UV_DIR)/uv +UVX ?= $(UV_DIR)/uvx +.PHONY: uv +uv: $(UV) ## Download uv +$(UV): $(TOOLS_DIR) + @test -s $(UV) || { mkdir -p $(UV_DIR); curl -LsSf https://astral.sh/uv/$(UV_VERSION)/install.sh | UV_INSTALL_DIR=$(UV_DIR) sh > /dev/null; } diff --git a/README.md b/README.md index 6224f3c..c71893e 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,12 @@ CircuitPython driver for the Semtech SX1280 LoRa chip (2.4 GHz) ## 🚧 Under construction. Use at your own risk! -### Status: -- [x] init and configure +### Status: +- [x] init and configure - [x] confirm Tx - [see example_tx](example_tx.py) - [x] confirm Rx - [x] exchange packet(s) - [x] confirm ranging - [ ] make library user friendly -NOTE: only LoRa aspects implemented thus far. lots of things are hard-coded in. Nearly all LoRa functionality is available (Tx, Rx, Ranging), but message buffer handling isn't streamlined yet. - +NOTE: only LoRa aspects implemented thus far. lots of things are hard-coded in. Nearly all LoRa functionality is available (Tx, Rx, Ranging), but message buffer handling isn't streamlined yet. diff --git a/example.py b/examples/example.py similarity index 84% rename from example.py rename to examples/example.py index 490d14a..b02e995 100644 --- a/example.py +++ b/examples/example.py @@ -10,4 +10,4 @@ spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO) -radio = sx1280.SX1280(spi, CS, RESET, BUSY) \ No newline at end of file +radio = sx1280.SX1280(spi, CS, RESET, BUSY) diff --git a/example_ranging_master.py b/examples/example_ranging_master.py similarity index 79% rename from example_ranging_master.py rename to examples/example_ranging_master.py index 5613d42..94e3e73 100644 --- a/example_ranging_master.py +++ b/examples/example_ranging_master.py @@ -1,4 +1,6 @@ -import board,time +import time + +import board import busio import digitalio @@ -6,7 +8,7 @@ CS = digitalio.DigitalInOut(board.D35) RESET = digitalio.DigitalInOut(board.D36) -BUSY = digitalio.DigitalInOut(board.D37) # lambda DIO0 +BUSY = digitalio.DigitalInOut(board.D37) # lambda DIO0 DIO1 = digitalio.DigitalInOut(board.D41) DIO2 = digitalio.DigitalInOut(board.D42) DIO3 = digitalio.DigitalInOut(board.D38) @@ -24,9 +26,9 @@ while True: radio.range() time.sleep(4) - status=radio.get_Irq_Status() + status = radio.get_Irq_Status() if status[2] > 0 or status[3] > 0: data1 = radio.read_range(raw=False) - print('Filtered:',data1) + print("Filtered:", data1) data2 = radio.read_range(raw=True) - print('Raw:\t',data2) \ No newline at end of file + print("Raw:\t", data2) diff --git a/example_ranging_slave.py b/examples/example_ranging_slave.py similarity index 81% rename from example_ranging_slave.py rename to examples/example_ranging_slave.py index 1eaa232..944c2df 100644 --- a/example_ranging_slave.py +++ b/examples/example_ranging_slave.py @@ -1,4 +1,6 @@ -import board,time +import time + +import board import busio import digitalio @@ -6,7 +8,7 @@ CS = digitalio.DigitalInOut(board.D35) RESET = digitalio.DigitalInOut(board.D36) -BUSY = digitalio.DigitalInOut(board.D37) # lambda DIO0 +BUSY = digitalio.DigitalInOut(board.D37) # lambda DIO0 DIO1 = digitalio.DigitalInOut(board.D41) DIO2 = digitalio.DigitalInOut(board.D42) DIO3 = digitalio.DigitalInOut(board.D38) @@ -25,5 +27,5 @@ time.sleep(5) status = radio.get_Irq_Status() if status[2] > 0 or status[3] > 0: - [print(hex(i)+' ',end='') for i in status] - print('') \ No newline at end of file + [print(hex(i) + " ", end="") for i in status] + print("") diff --git a/example_rx.py b/examples/example_rx.py similarity index 83% rename from example_rx.py rename to examples/example_rx.py index 2f7c50c..ddd0dba 100644 --- a/example_rx.py +++ b/examples/example_rx.py @@ -1,8 +1,10 @@ -''' +""" Working RX example for SX1280 breakout using SAM32 -''' +""" -import board,time +import time + +import board import busio import digitalio @@ -10,7 +12,7 @@ CS = digitalio.DigitalInOut(board.D35) RESET = digitalio.DigitalInOut(board.D36) -BUSY = digitalio.DigitalInOut(board.D37) # lambda DIO0 +BUSY = digitalio.DigitalInOut(board.D37) # lambda DIO0 DIO1 = digitalio.DigitalInOut(board.D41) DIO2 = digitalio.DigitalInOut(board.D42) DIO3 = digitalio.DigitalInOut(board.D31) @@ -28,6 +30,6 @@ while True: msg = radio.receive() - if msg != None: + if msg is not None: print(msg, radio.packet_status) - time.sleep(1) \ No newline at end of file + time.sleep(1) diff --git a/example_tx.py b/examples/example_tx.py similarity index 77% rename from example_tx.py rename to examples/example_tx.py index 534f203..5da5775 100644 --- a/example_tx.py +++ b/examples/example_tx.py @@ -1,8 +1,10 @@ -''' +""" Working TX example for SX1280 breakout using SAM32 -''' +""" -import board,time +import time + +import board import busio import digitalio @@ -10,7 +12,7 @@ CS = digitalio.DigitalInOut(board.D35) RESET = digitalio.DigitalInOut(board.D36) -BUSY = digitalio.DigitalInOut(board.D37) # lambda DIO0 +BUSY = digitalio.DigitalInOut(board.D37) # lambda DIO0 DIO1 = digitalio.DigitalInOut(board.D41) DIO2 = digitalio.DigitalInOut(board.D42) DIO3 = digitalio.DigitalInOut(board.D31) @@ -23,8 +25,8 @@ radio = sx1280.SX1280(spi, CS, RESET, BUSY, debug=False) -cnt=0 +cnt = 0 while True: - cnt+=1 - radio.send('ping'+str(cnt)) - time.sleep(1) \ No newline at end of file + cnt += 1 + radio.send("ping" + str(cnt)) + time.sleep(1) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d33352f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "proves-circuitpython-sx1280" +version = "2.0.0" +description = "Flight Software for the PROVES Kit" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "adafruit-circuitpython-typing==1.11.2", + "circuitpython-stubs==9.2.5", + "pyright[nodejs]==1.1.399", + "pre-commit==4.0.1", +] + +[tool.setuptools] +packages = [ + "sx1280", +] + +[tool.ruff.format] +# Use `\n` line endings for all files +line-ending = "lf" + +[tool.pyright] +include = ["sx1280.py"] +exclude = [ + "**/__pycache__", + ".venv", + ".git", + "examples", + "typings", +] +stubPath = "./typings" +reportMissingModuleSource = false diff --git a/sx1280.py b/sx1280.py deleted file mode 100644 index 9c73a29..0000000 --- a/sx1280.py +++ /dev/null @@ -1,978 +0,0 @@ -from time import sleep,monotonic -import digitalio -from micropython import const -import adafruit_bus_device.spi_device as spidev -from random import random - -# Radio Commands -_RADIO_GET_STATUS = const(0xC0) -_RADIO_WRITE_REGISTER = const(0x18) -_RADIO_READ_REGISTER = const(0x19) -_RADIO_WRITE_BUFFER = const(0x1A) -_RADIO_READ_BUFFER = const(0x1B) -_RADIO_SET_SLEEP = const(0x84) -_RADIO_SET_STANDBY = const(0x80) -_RADIO_SET_TX = const(0x83) -_RADIO_SET_RX = const(0x82) -_RADIO_SET_CAD = const(0xC5) -_RADIO_SET_PACKETTYPE = const(0x8A) -_RADIO_GET_PACKETTYPE = const(0x03) -_RADIO_SET_RFFREQUENCY = const(0x86) -_RADIO_SET_TXPARAMS = const(0x8E) -_RADIO_SET_CADPARAMS = const(0x88) -_RADIO_SET_BUFFERBASEADDRESS = const(0x8F) -_RADIO_SET_MODULATIONPARAMS = const(0x8B) -_RADIO_SET_PACKETPARAMS = const(0x8C) -_RADIO_GET_RXBUFFERSTATUS = const(0x17) -_RADIO_GET_PACKETSTATUS = const(0x1D) -_RADIO_GET_RSSIINST = const(0x1F) -_RADIO_SET_DIOIRQPARAMS = const(0x8D) -_RADIO_GET_IRQSTATUS = const(0x15) -_RADIO_CLR_IRQSTATUS = const(0x97) -_RADIO_SET_REGULATORMODE = const(0x96) -_RADIO_SET_AUTOFS = const(0x9E) -_RADIO_SET_RANGING_ROLE = const(0xA3) -_PACKET_TYPE_LORA = const(0x01) -_PACKET_TYPE_RANGING = const(0x02) -_PACKET_HEADER_EXPLICIT = const(0x00) # varriable length, header included -_PACKET_HEADER_IMPLICIT = const(0x80) # fixed length, no header in packet -_PACKET_CRC_MODE_ON = const(0x20) # 32 -_PACKET_CRC_MODE_OFF = const(0x00) -_PACKET_IQ_INVERT = const(0x00) -_PACKET_IQ_NORMAL = const(0x40) # 64 -_XTAL_FREQ = const(52000000) -_FREQ_STEP = _XTAL_FREQ/262144 - -# Radio Head Constants -_RH_BROADCAST_ADDRESS = const(0xFF) -_RH_FLAGS_ACK = const(0x80) -_RH_FLAGS_RETRY = const(0x40) - -_irq1Def=('RngSlaveReqDiscard','RngMasterResultValid','RngMasterTimeout','RngMasterReqValid','CadDone','CadDetected','RxTxTimeout','AdvRngDone') -_irq2Def=('TxDone','RxDone','SyncWrdValid','SyncWrdErr','HeaderValid','HeaderErr','CrcErr','RngSlaveResponseDone') -# _ranging_calibrations={# SF5 SF6 SF7 SF8 SF9 SF10 -# 'BW400' :[const(10299),const(10271),const(10244),const(10242),const(10230),const(10246)], -# 'BW800' :[const(11486),const(11474),const(11453),const(11426),const(11417),const(11401)], -# 'BW1600':[const(13308),const(13493),const(13528),const(13515),const(13430),const(13376)]} -# range_Freqoff={ # SF5 SF6 SF7 SF8 SF9 SF10 -# 'BW400' :[-0.148,-0.214,-0.419,-0.853,-1.686,-3.423], -# 'BW800' :[-0.041,-0.811,-0.218,-0.429,-0.853,-1.737], -# 'BW1600':[0.103, -0.041,-0.101,-0.211,-0.424,-0.87 ]} -_mode_mask = const(0xE0) -_cmd_stat_mask = const(0x1C) - -class SX1280: - _status=bytearray(1) - _status_msg={'mode':'','cmd':'','busy':False} - _status_mode={0:'N/A', - 1:'N/A', - 2:'STDBY_RC', - 3:'STDBY_XOSC', - 4:'FS', - 5:'Rx', - 6:'Tx'} - _status_cmd={0:'N/A', - 1:'Cmd Successful', - 2:'Data Available', - 3:'Timed-out', - 4:'Cmd Error', - 5:'Failure to Execute Cmd', - 6:'Tx Done'} - _BUFFER = bytearray(10) - - - pcktparams={ - 'PreambleLength':12, - 'HeaderType' :_PACKET_HEADER_EXPLICIT, - 'PayloadLength' :0x0F, - 'CrcMode' :_PACKET_CRC_MODE_ON, - 'InvertIQ' :_PACKET_IQ_NORMAL} - ranging_params={'SF':0xA0,'BW':0x0A,'CR':0x01} - - def __init__(self, spi, cs, reset, busy, frequency, *, preamble_length=8, baudrate=1500000, debug=False, txen=False, rxen=False): - # self._device = spidev.SPIDevice(spi, cs, baudrate=baudrate, polarity=0, phase=0) - self._device = spidev.SPIDevice(spi, cs, baudrate=500000, polarity=0, phase=0) - self._reset = reset - self._reset.switch_to_input(pull=digitalio.Pull.UP) - self._busy = busy - self._busy.switch_to_input() - self.packet_type = 0 # default - self._debug = debug - self.default_dio=False - self.txen=txen - self.rxen=rxen - self.frequency=frequency - self.ranging_calibration=False - self.rng_rssi=0 - - - self.reset() - self._busywait() - self.retry_counter=0 - self.timeouts=0 - - # Radio Head (RH) Stuff - self.ack_delay = None - self.ack_retries = 5 - self.ack_wait = 0.2 - self.sequence_number = 0 - # self.seen_ids = bytearray(256) - - # RH Header Bytes - self.node = _RH_BROADCAST_ADDRESS - self.destination = _RH_BROADCAST_ADDRESS - self.identifier = 0 - self.flags = 0 - - # default register configuration - self.default_config() - - def default_config(self): - self.sleeping=False - self._set_ranging = False - self._ranging=False - self._status = 0 - self._autoFS=False - self._listen=False - self._range_listening=False - self.set_Standby('STDBY_RC') - self.clear_Irq_Status() - self.set_Regulator_Mode() - self.set_Packet_Type() # set to LoRa - self.frequency_ghz=self.frequency - self.set_Modulation_Params() - self.set_Packet_Params() - self.set_Buffer_Base_Address() - self.set_Tx_Param() # power=13dBm,rampTime=20us - self.high_sensitivity_lna(True) - self.set_Dio_IRQ_Params() - # Set Ranging Filter Size to 200 samples - # self._writeRegister(0x9,0x1E,20) - # Disable Advanced Ranging - self._send_command(bytes([0x9A,0])) - # Disable long preamble - # self._send_command(bytes([0x9B,0])) - # Set Save Context - self._send_command(bytes([0xD5])) - - if self._debug: print('Radio Initialized') - - def _convert_status(self,status): - mode = (status & _mode_mask)>>5 - cmdstat = (status & _cmd_stat_mask)>>2 - if mode in self._status_mode: - self._status_msg['mode']=self._status_mode[mode] - if cmdstat in self._status_cmd: - self._status_msg['cmd']=self._status_cmd[cmdstat] - self._status_msg['busy'] = not bool(status & 0x1) - return self._status_msg - - def _send_command(self,command,stat=False): - _size=len(command) - self._busywait() - with self._device as device: - device.write_readinto(command,self._BUFFER,out_end=_size,in_end=_size) - if stat: - print('SendCMD CMD:',[hex(i) for i in command]) - print('SendCMD BUF:',[hex(i) for i in self._BUFFER]) - if self._debug: - print('\t\t{}'.format(self._convert_status(self._BUFFER[0]))) - self._busywait() - return self._BUFFER[:_size] - - def _writeRegister(self,address1,address2,data): - if self._debug: - print('Writing to:',hex(address1),hex(address2)) - self._send_command(bytes([_RADIO_WRITE_REGISTER,address1,address2,data])) - - def _readRegister(self,address1,address2): - if self._debug: - print('Reading:',hex(address1),hex(address2)) - self._busywait() - with self._device as device: - device.write(bytes([_RADIO_READ_REGISTER,address1,address2]), end=3) - device.readinto(self._BUFFER, end=2) - if self._debug: - [print(hex(i),' ',end='')for i in self._BUFFER] - print('') - self._busywait() - return self._BUFFER[1] # TODO this seems wrong - def _readRegisters(self,address1,address2,_length=1): - ''' - read_reg_cmd, addr[15:8], addr[7:0], NOP, NOP, NOP, NOP, ... - status status status status data1 data2 data3 ... - ''' - _size = _length + 4 - if self._debug: print('Reading:',hex(address1),hex(address2)) - self._busywait() - with self._device as device: - device.write_readinto(bytes([_RADIO_READ_REGISTER,address1,address2,0]+[0]*_length),self._BUFFER,out_end=_size,in_end=_size) - self._busywait() - if self._debug: print('Read Regs ({}, {}) _BUFFER: {}'.format(hex(address1),hex(address2),[hex(i) for i in self._BUFFER])) - return self._BUFFER[4:_size] - - def printRegisters(self,start=0x0900,stop=0x09FF,p=True): - _length=stop-start+1 - _size=_length+4 - buff=bytearray(_size) - self._busywait() - with self._device as device: - device.write_readinto(bytes([_RADIO_READ_REGISTER])+int(start).to_bytes(2,'big')+b'\x00'+b'\x00'*_length,buff,out_end=_size,in_end=_size) - if p: - pass - else: - return buff - - # @timeout(3) - def _busywait(self): - _t=monotonic()+3 - while monotonic()<_t: - if self._debug: print('waiting for busy pin.') - if not self._busy.value: - return True - print('TIMEDOUT on busywait') - self.timeouts+=1 - if self.timeouts > 5: - self.timeouts=0 - try: - with open('/sd/log.txt','a') as f: - f.write('sb:{}\n'.format(int(monotonic()))) - except: pass - self.reset_io() - if hasattr(self,'timeout_handler'): self.timeout_handler() - return False - - def reset_io(self): - self._reset.switch_to_output(value=False) - self._busy.switch_to_input() - sleep(5) - self._reset.switch_to_input(pull=digitalio.Pull.UP) - sleep(2) - self.default_config() - - def wait_for_irq(self): - if self.default_dio: - return self.DIOwait(self.default_dio) - else: - return self.IRQwait(bit=1) - - # @timeout(3) - def DIOwait(self,pin): - _t=monotonic()+3 - while monotonic()<_t: - if pin.value: - self.clear_Irq_Status() - return True - print('TIMEDOUT on DIOwait') - if hasattr(self,'timeout_handler'): self.timeout_handler() - return False - - # @timeout(4) - def IRQwait(self,bit): - _t=monotonic()+4 - while monotonic()<_t: - _irq=self.get_Irq_Status(clear=False)[1] & 1 - # print(hex(_irq)) - if (_irq >> bit) & 1: - return True - print('TIMEDOUT on IRQwait') - if hasattr(self,'timeout_handler'): self.timeout_handler() - return False - - def set_Regulator_Mode(self,mode=0x01): - if self._debug: - print('Setting Regulator Mode') - self._send_command(bytes([_RADIO_SET_REGULATORMODE, mode])) - - def reset(self): - self._reset.switch_to_output(value=False) - sleep(0.05) # 50 ms - self._reset.switch_to_input(pull=digitalio.Pull.UP) - sleep(0.03) # 30 ms - - def set_Standby(self,state='STDBY_RC'): - if self._debug: print('Setting Standby') - if state == 'STDBY_RC': - self._send_command(bytes([_RADIO_SET_STANDBY, 0x00])) - elif state == 'STDBY_XOSC': - self._send_command(bytes([_RADIO_SET_STANDBY, 0x01])) - - def sleep(self): - self._busywait() - with self._device as device: - device.write_readinto(bytes([_RADIO_SET_SLEEP, 0x07]),self._BUFFER,out_end=2,in_end=2) - self.sleeping=True - if self._debug: - print('\t\tSleeping SX1280') - print('\t\t{}'.format(self._convert_status(self._BUFFER[0]))) - - def wake_up(self): - if self.sleeping: - if self._debug: print('\t\tWaking SX1280') - with self._device as device: - sleep(0.1) - device.write(bytes([0xC0,0])) # send no-op to wake-up - if not self._busywait(): - print('wake_up busy fail') - # reinitalize - self.default_config() - - def set_Packet_Type(self,packetType=_PACKET_TYPE_LORA): - self._packetType = packetType - if packetType == 'RANGING': - self._packetType = _PACKET_TYPE_RANGING - - if self._debug: - print('Setting Packet Type') - self._send_command(bytes([_RADIO_SET_PACKETTYPE, self._packetType])) - self.packet_type = packetType - - def set_Cad_Params(self,symbol=0x80): - if self._debug: print('Setting CAD Parameters') - self._send_command(bytes([_RADIO_SET_CADPARAMS, symbol])) - - def set_Cad(self): - if self._debug: print('Setting CAD Search') - self._send_command(bytes([_RADIO_SET_CAD])) - self.clear_Irq_Status() - @property - def frequency_ghz(self): - return float(str(self.frequency)+"E"+"9") - - @frequency_ghz.setter - def frequency_ghz(self,freq): - ''' - 0xB89D89 = 12098953 PLL steps = 2.4 GHz - 2.4 GHz = 12098953*(52000000/(2**18)) - ''' - self.frequency=freq - _f=int(float(str(freq)+"E"+"9")/_FREQ_STEP) - if self._debug: print('\t\tSX1280 freq: {:G} GHz ({} PLL steps)'.format(float(str(freq)+"E"+"9"),_f)) - self._send_command(bytes([_RADIO_SET_RFFREQUENCY,(_f>>16)&0xFF,(_f>>8)&0xFF,_f&0xFF])) - - def set_Buffer_Base_Address(self,txBaseAddress=0x00,rxBaseAddress=0x00): - if self._debug: print('Setting Buffer Base Address') - self._txBaseAddress = txBaseAddress - self._rxBaseAddress = rxBaseAddress - self._send_command(bytes([_RADIO_SET_BUFFERBASEADDRESS, txBaseAddress, rxBaseAddress])) - - def set_Modulation_Params(self,modParam1=0x70,modParam2=0x26,modParam3=0x01): - # LoRa: modParam1=SpreadingFactor, modParam2=Bandwidth, modParam3=CodingRate - # LoRa with SF7, (BW1600=0x0A -> changed to BW400=0x26), CR 4/5 - # Must set PacketType first! - See Table 13-48,49,50 - if self._debug: print('Setting Modulation parameters') - self._send_command(bytes([_RADIO_SET_MODULATIONPARAMS,modParam1,modParam2,modParam3])) - if self.packet_type == _PACKET_TYPE_LORA: - self._busywait() - # If the Spreading Factor selected is SF5 or SF6 - if modParam1 in (0x50,0x60): - self._writeRegister(0x09,0x25,0x1E) - # If the Spreading Factor is SF7 or SF-8 - elif modParam1 in (0x70,0x80): - self._writeRegister(0x09,0x25,0x37) - # If the Spreading Factor is SF9, SF10, SF11 or SF12 - elif modParam1 in (0x90,0xA0,0xB0,0xC0): - self._writeRegister(0x09,0x25,0x32) - else: - print('Invalid Spreading Factor') - - def set_Packet_Params(self): - if self._debug: print(self.pcktparams) - self._send_command(bytes([_RADIO_SET_PACKETPARAMS, - self.pcktparams['PreambleLength'], - self.pcktparams['HeaderType'], - self.pcktparams['PayloadLength'], - self.pcktparams['CrcMode' ], - self.pcktparams['InvertIQ'], - 0x00,0x00])) - - def set_Tx_Param(self,power=0x1F,rampTime=0xE0): - # power=13 dBm (0x1F), rampTime=20us (0xE0). See Table 11-47 - # P=-18+power -18+0x1F=13 - if self._debug: - print('Setting Tx Parameters') - self._send_command(bytes([_RADIO_SET_TXPARAMS,power,rampTime])) - - def write_Buffer(self,data): - #Offset will correspond to txBaseAddress in normal operation. - _offset = self._txBaseAddress - _len = len(data) - assert 0 < _len <= 252 - self._busywait() - with self._device as device: - device.write(bytes([_RADIO_WRITE_BUFFER,_offset])+data,end=_len+2) - - def read_Buffer(self,offset,payloadLen): - _payload = bytearray(payloadLen) - self._busywait() - with self._device as device: - device.write(bytes([_RADIO_READ_BUFFER,offset]), end=2) - device.readinto(_payload, end=payloadLen) - return _payload - - def dump_buffer(self,dbuffer): - self._busywait() - with self._device as device: - device.write(bytes([_RADIO_READ_BUFFER,0,0]), end=3) - device.readinto(dbuffer) - # print('Status:',self._convert_status(self._BIGBUFFER[0])) - # [print(hex(i),end=',') for i in self._BIGBUFFER[1:]] - # print('') - - def get_bw(self,_bw): - if _bw==0x0A: - bw_hz=1625000 - elif _bw==0x18: - bw_hz= 812500 - elif _bw==0x26: - bw_hz= 406250 - elif _bw==0x34: - bw_hz= 203125 - else: - print('bad BW conversion') - return 0 - return bw_hz - - - def set_Dio_IRQ_Params(self,irqMask=[0xFF,0xF3],dio1Mask=[0x00,0x03],dio2Mask=[0x00,0x02],dio3Mask=[0x40,0x20]): - ''' - TxDone IRQ on DIO1, RxDone IRQ on DIO2, HeaderError and RxTxTimeout IRQ on DIO3 - IRQmask (bit[0]=TxDone, bit[1]=RxDone) - 0x43: 0x23 - 0100 0011 0010 0011 - DIO1mask - 0000 0000 0000 0001 - DIO2mask - 0000 0000 0000 0010 - ''' - if self._debug: print('Setting DIO IRQ Parameters') - self._send_command(bytes([_RADIO_SET_DIOIRQPARAMS]+irqMask+dio1Mask+dio2Mask+dio3Mask)) - - def clear_Irq_Status(self, val=[0xFF,0xFF]): - if self._debug: print('Clearing IRQ Status') - self._send_command(bytes([_RADIO_CLR_IRQSTATUS]+val)) - - def get_Irq_Status(self,clear=[0xFF,0xFF],parse=False,debug=False): - if self._debug: print('Getting IRQ Status') - _irq1,_irq2 = self._send_command(bytes([_RADIO_GET_IRQSTATUS,0x00,0x00,0x00]))[2:] - - if parse: - if self._debug: print('IRQ[15:8]:{}, IRQ[7:0]:{}'.format(hex(_irq1),hex(_irq2))) # - _rslt=[] - for i,j in zip(reversed('{:08b}'.format(_irq1)),_irq1Def): # [15:8] - if int(i): - _rslt.append(j) - for i,j in zip(reversed('{:08b}'.format(_irq2)),_irq2Def): # [7:0] - if int(i): - _rslt.append(j) - if debug: print('IRQ Results: {}'.format(_rslt)) - return((_rslt,hex(_irq1),hex(_irq2))) - - if clear: - if clear==True: - clear=[0xFF,0xFF] - self._send_command(bytes([_RADIO_CLR_IRQSTATUS]+clear)) # clear IRQ status - return (_irq1,_irq2) - - def set_Tx(self,pBase=0x02,pBaseCount=[0x00,0x00]): - #Activate transmit mode with no timeout. Tx mode will stop after first packet sent. - if self._debug: print('Setting Tx') - # self.clear_Irq_Status([8,7]) - self.clear_Irq_Status() - self._send_command(bytes([_RADIO_SET_TX, pBase, pBaseCount[0], pBaseCount[1]])) - self._listen=False - - def set_Rx(self,pBase=0x02,pBaseCount=[0xFF,0xFF]): - ''' - pBaseCount = 16 bit parameter of how many steps to time-out - see Table 11-22 for pBase values (0xFFFF=continuous) - Time-out duration = pBase * periodBaseCount - ''' - if self._debug: print('\tSetting Rx') - # self.clear_Irq_Status([8,7]) - self.clear_Irq_Status() - self._send_command(bytes([_RADIO_SET_RX, pBase]+pBaseCount)) - - def set_autoFS(self,value): - self._send_command(bytes([_RADIO_SET_AUTOFS, bool(value)])) - self._autoFS=value - - def high_sensitivity_lna(self,enabled=True): - _reg=self._readRegister(0x8,0x91) - if enabled: - self._writeRegister(0x8,0x91,_reg | 0xC0) - else: - self._writeRegister(0x8,0x91,_reg & 0x3F) - - def clear_range_samples(self): - # to clear, set bit 5 to 1 then to 0 - _reg=self._readRegister(0x9,0x23) - # print('Register 0x923:',hex(_reg)) - _reg |= (1 << 5) - # print('Register 0x923:',hex(_reg)) - self._writeRegister(0x9,0x23,_reg) - _reg &= ~(1 << 5) - # print('Register 0x923:',hex(_reg)) - self._writeRegister(0x9,0x23,_reg) - - def set_Ranging_Params(self,range_addr=[0x01,0x02,0x03,0x04], master=False, slave=False): - self.set_Standby('STDBY_RC') - self.clear_range_samples() - self.set_Packet_Type('RANGING') - self.set_Modulation_Params(modParam1=self.ranging_params['SF'],modParam2=self.ranging_params['BW'],modParam3=self.ranging_params['CR']) - self.pcktparams['PreambleLength']=12 - self.pcktparams['PayloadLength']=0 - self.set_Packet_Params() - self.frequency_ghz=self.frequency - self.set_Buffer_Base_Address(txBaseAddress=0x00,rxBaseAddress=0x00) - self.set_Tx_Param() # DEFAULT:power=13dBm,rampTime=20us - if slave: - self._rangingRole = 0x00 - # Slave Ranging address - self._writeRegister(0x9,0x19,range_addr[0]) - self._writeRegister(0x9,0x18,range_addr[1]) - self._writeRegister(0x9,0x17,range_addr[2]) - self._writeRegister(0x9,0x16,range_addr[3]) - # Ranging address length - self._writeRegister(0x9,0x31,0x3) - # self.set_Dio_IRQ_Params(irqMask=[0x7F,0xF3],dio1Mask=[0x00,0x83],dio2Mask=[0x00,0x03],dio3Mask=[0x40,0x20]) # wrong? RangingSlaveResponseDone,RxDone,TxDone - # self.set_Dio_IRQ_Params(irqMask=[0x7F,0xF3],dio1Mask=[0x9,0x80],dio2Mask=[0x00,0x03],dio3Mask=[0x40,0x20]) # RangingMasterRequestValid,RangingSlaveRequestDiscard,RangingSlaveResponseDone - self.set_Dio_IRQ_Params(irqMask=[0x7F,0xF3],dio1Mask=[0x1,0x80],dio2Mask=[0x00,0x03],dio3Mask=[0x40,0x20]) # RangingSlaveRequestDiscard,RangingSlaveResponseDone - elif master: - self._rangingRole = 0x01 - # Master Ranging address - self._writeRegister(0x9,0x15,range_addr[0]) - self._writeRegister(0x9,0x14,range_addr[1]) - self._writeRegister(0x9,0x13,range_addr[2]) - self._writeRegister(0x9,0x12,range_addr[3]) - # self.set_Dio_IRQ_Params(irqMask=[0x7F,0xF3],dio1Mask=[0x7,0x80],dio2Mask=[0x00,0x01],dio3Mask=[0x00,0x00]) # wrong? RangingMasterTimeout,RangingMasterResultValid,RangingSlaveRequestDiscard,RangingSlaveResponseDone - self.set_Dio_IRQ_Params(irqMask=[0x7F,0xF3],dio1Mask=[0x6,0x00],dio2Mask=[0x00,0x01],dio3Mask=[0x00,0x00]) # RangingMasterTimeout,RangingMasterResultValid - - else: - print('Select Master or Slave Only') - return False - - # Set DIO IRQ Parameters - self.clear_Irq_Status() - - if self.ranging_calibration == 'custom': - self.set_Ranging_Calibration(custom=self.rxtxdelay) - elif self.ranging_calibration: - self.set_Ranging_Calibration(zero=True) - else: - # Set Ranging Calibration per Section 3.3 of SemTech AN1200.29 - # TODO set based on modulation params - # self.set_Ranging_Calibration(CAL='BW1600',SF=5) - # self.set_Ranging_Calibration(CAL='BW1600',SF=9) - self.set_Ranging_Calibration(CAL='BW1600',SF=10) - - # Set Ranging Role - self._send_command(bytes([_RADIO_SET_RANGING_ROLE, self._rangingRole])) - - self.high_sensitivity_lna(True) - - self._set_ranging = True - - def stop_ranging(self): - self.set_Standby('STDBY_RC') - self.set_Packet_Type() # set to LoRa - self.set_Packet_Params() - self.high_sensitivity_lna(True) - self.set_Dio_IRQ_Params() - if self.txen: - self.txen.value=False - self.rxen.value=False - self._set_ranging = False - - def read_range(self,raw=True,raw_bytes=False): - if not self._ranging: - print('Start ranging before attempting to read') - return - - self.set_Standby('STDBY_XOSC') - #enable LoRa modem clock - _temp=self._readRegister(0x9,0x7F) | (1 << 1) - self._writeRegister(0x9,0x7F,_temp) - # Set the ranging type for filtered or raw - _conf=self._readRegister(0x9,0x24) - if raw: - _conf = (_conf & 0xCF) | 0x0 - else: - # _conf = (_conf & 0xCF) | 0x10 # averaged - # _conf = (_conf & 0xCF) | 0x20 # debiased - _conf = (_conf & 0xCF) | 0x30 # filtered - # print('Range Data Type:',hex(_conf)) - self._writeRegister(0x9,0x24,_conf) - - # Read the 24-bit value - self._rbuff=bytearray(4) - self._rbuff=self._readRegisters(0x9,0x61,4) # confirmed working - self.rng_rssi=-1*self._rbuff[3]/2 - # print('rng_rssi: {}'.format(self.rng_rssi)) - - self.set_Standby('STDBY_RC') - - if raw_bytes: - return self._rbuff[:3] - - _val = 0 | (self._rbuff[0] << 16) - _val |= (self._rbuff[1] << 8) - _val |= (self._rbuff[2]) - - # dist in meters = _val * 150/(2^12 * BW in MHz) = 2scomp / (BW in Hz * 36621.09375) - _2scomp = self.complement2(_val,24) / self.get_bw(self.ranging_params['BW']) * 36621.09375 - if raw: return _2scomp - # averaged, debiased, or filtered results - return _2scomp * 20.0 / 100.0 - - def get_Freq_Error_Indicator(self): - # Read the 20-bit value (based on StuartsProjects github implementation) - self.set_Standby('STDBY_XOSC') - efeRaw=self._readRegisters(0x9,0x54,3) - efeRaw[0]=efeRaw[0]&0x0F #clear bit 20 which is always set - self.set_Standby() - return efeRaw - - def set_Ranging_Calibration(self,CAL='BW1600',SF=5,zero=False,custom=False): - if zero: - CAL=0 - elif custom: - CAL=custom - else: - CAL=0 - self._writeRegister(0x9,0x2D,CAL&0xFF) # cal[7:0] - self._writeRegister(0x9,0x2C,(CAL>>8)&0xFF) # cal[15:8] - - def calc_efe(self,efeRaw): - efe = 0 | (efeRaw[0]<< 16) - efe |= (efeRaw[1]<< 8) - efe |= (efeRaw[2]) - # efe &= 0x0FFFFF # now performed in get_Freq_Error_Indicator step - efeHz = 1.55 * self.complement2(efe,20) / (1625000/self.get_bw(self.ranging_params['BW'])) - return efeHz - - def complement2(self,num,bitCnt): - retVal = num - if retVal >= 2<<(bitCnt-2): - retVal -= 2<<(bitCnt-1) - return retVal - - def get_Packet_Status(self): - # See Table 11-63 - self._packet_status = [] - p_stat = self._send_command(bytes([_RADIO_GET_PACKETSTATUS,0x00,0x00,0x00,0x00,0x00,0x00])) - # [print(hex(i)+' ',end='') for i in self._BUFFER[:6]] - self.rssiSync = (-1*int(p_stat[2])/2) - self.snr = (int(p_stat[3])/4) - return p_stat - - def get_Rx_Buffer_Status(self): - self._send_command(bytes([_RADIO_GET_RXBUFFERSTATUS,0x00,0x00,0x00])) - return self._BUFFER[:4] - - def send(self, data, pin=None,irq=False,header=True,ID=0,target=0,action=0,keep_listening=False): - """Send a string of data using the transmitter. - You can only send 252 bytes at a time - (limited by chip's FIFO size and appended headers). - """ - return self.send_mod(data,keep_listening=keep_listening,header=header) - - - @property - def packet_status(self): - self.get_Packet_Status() - return (self.rssiSync,self.snr) - - @property - def listen(self): - return self._listen - - @listen.setter - def listen(self, enable): - if enable: - if not self._listen: - if self.rxen: - self.txen.value=False - self.rxen.value=True - self.set_Rx() - self._listen = True - else: - if self.rxen: - self.rxen.value=False - self.set_Standby('STDBY_RC') - self._listen = False - - def receive(self, continuous=True, keep_listening=True): - if not self._listen: - self.listen = True - if continuous: - self._buf_status = self.get_Rx_Buffer_Status() - self._packet_len = self._buf_status[2] - self._packet_pointer = self._buf_status[3] - if self._packet_len > 0: - if self._debug: - print('Offset:',self._packet_pointer,'Length:',self._packet_len) - packet = self.read_Buffer(offset=self._packet_pointer,payloadLen=self._packet_len+1) - if not keep_listening: - self.listen = False - return packet[1:] - - @property - def packet_info(self): - return (self._packet_len,self._packet_pointer) - - def rssi(self,raw=False): - self._rssi = self._send_command(bytes([_RADIO_GET_PACKETSTATUS,0x00,0x00])) - if raw: - return self._rssi[-1] - else: - return -1*self._rssi[-1]/2 # dBm - - def status(self, raw=False): - self._busywait() - with self._device as device: - device.write_readinto(bytes([_RADIO_GET_STATUS]),self._BUFFER,out_end=1,in_end=1) - # if raw: print([hex(i) for i in self._BUFFER]) - if raw: - return self._BUFFER[0] - self._busywait() - return self._convert_status(self._BUFFER[0]) - - def send_mod( - self, - data, - *, - keep_listening=False, - header=False, - destination=None, - node=None, - identifier=None, - flags=None, - debug=False): - data_len = len(data) - assert 0 < data_len <= 252 - if self.txen: - self.rxen.value=False - self.txen.value=True - if debug: print('\t\ttxen:on, rxen:off') - if header: - payload = bytearray(4) - if destination is None: # use attribute - payload[0] = self.destination - else: # use kwarg - payload[0] = destination - if node is None: # use attribute - payload[1] = self.node - else: # use kwarg - payload[1] = node - if identifier is None: # use attribute - payload[2] = self.identifier - else: # use kwarg - payload[2] = identifier - if flags is None: # use attribute - payload[3] = self.flags - else: # use kwarg - payload[3] = flags - if debug: print('HEADER: {}'.format([hex(i) for i in payload])) - data = payload + data - data_len+=4 - # Configure Packet Length - self.pcktparams['PayloadLength']=data_len - self.set_Packet_Params() - self.write_Buffer(data) - self.set_Tx() - txdone=self.wait_for_irq() - if keep_listening: - self.listen=True - else: - if self.txen: - self.txen.value=False - if debug: print('\t\ttxen:off, rxen:n/a') - return txdone - - def receive_mod( - self, *, keep_listening=True, with_header=False, with_ack=False, timeout=0.5,debug=False): - timed_out = False - if not self.default_dio: - print('must set default DIO!') - return False - if timeout is not None: - if not self._listen: - self.listen = True - start = monotonic() - timed_out = False - # Blocking wait for interrupt on DIO - while not timed_out and not self.default_dio.value: - if (monotonic() - start) >= timeout: - timed_out = True - # Radio has received something! - packet = None - # Stop receiving other packets - self.listen=False - if not timed_out: - self._buf_status = self.get_Rx_Buffer_Status() - self._packet_len = self._buf_status[2] - self._packet_pointer = self._buf_status[3] - if self._packet_len > 0: - packet = self.read_Buffer(offset=self._packet_pointer,payloadLen=self._packet_len+1)[1:] - self.clear_Irq_Status() - if self._packet_len > 4: - if (self.node != _RH_BROADCAST_ADDRESS - and packet[0] != _RH_BROADCAST_ADDRESS - and packet[0] != self.node): - if debug: print('Overheard packet:',packet) - packet = None - # send ACK unless this was an ACK or a broadcast - elif (with_ack - and ((packet[3] & _RH_FLAGS_ACK) == 0) - and (packet[0] != _RH_BROADCAST_ADDRESS)): - # delay before sending Ack to give receiver a chance to get ready - if self.ack_delay is not None: - sleep(self.ack_delay) - self.send_mod( - b"!", - keep_listening=keep_listening, - header=True, - destination=packet[1], - node=packet[0], - identifier=packet[2], - flags=(packet[3] | _RH_FLAGS_ACK)) - # print('sband ack to {}'.format(packet[1])) - if not with_header: # skip the header if not wanted - packet = packet[4:] - # Listen again if necessary and return the result packet. - if keep_listening: - self.listen=True - return packet - - def send_with_ack(self, data): - """Reliable Datagram mode: - Send a packet with data and wait for an ACK response. - The packet header is automatically generated. - If enabled, the packet transmission will be retried on failure - """ - if self.ack_retries: - retries_remaining = self.ack_retries - else: - retries_remaining = 1 - got_ack = False - self.retry_counter=0 - self.sequence_number = (self.sequence_number + 1) & 0xFF - while not got_ack and retries_remaining: - self.identifier = self.sequence_number - self.send_mod(data, header=True, keep_listening=True) - # Don't look for ACK from Broadcast message - if self.destination == _RH_BROADCAST_ADDRESS: - print('sband destination=RHbroadcast address (dont look for ack)') - got_ack = True - else: - # wait for a packet from our destination - ack_packet = self.receive_mod(timeout=self.ack_wait, with_header=True) - if ack_packet is not None: - if ack_packet[3] & _RH_FLAGS_ACK: - # check the ID - if ack_packet[2] == self.identifier: - got_ack = True - break - else: - print('bad sband ack ID. Looking for: {}'.format(hex(self.identifier))) - # delay random amount before retry - if not got_ack: - self.retry_counter+=1 - print('no sband ack, sending again...') - sleep(self.ack_wait + self.ack_wait * random()) - retries_remaining = retries_remaining - 1 - # set retry flag in packet header - self.flags |= _RH_FLAGS_RETRY - self.flags = 0 # clear flags - return got_ack - - def get_range(self,addr=[0,0,0,0],raw=False,timeout=10,t_resend=3,debug=False,delay=1): - timed_out = False - irq=[] - if not self.default_dio: - print('must set default DIO!') - return False - # sleep a delayed amount to give slave time to configure - sleep(delay) - self.set_Ranging_Params(range_addr=addr,master=True) - self.set_Tx(pBase=0x02,pBaseCount=[0xFF,0xFF]) # reduced pbase to 1ms - - if timeout is not None: - resend = monotonic()+t_resend - timed_out = monotonic()+timeout - while monotonic() < timed_out: - if self.default_dio.value: - irq=self.get_Irq_Status(clear=True,parse=True)[0] - if irq: - # print('m',irq) - if debug: print(irq) - if 'RngMasterResultValid' in irq: - self._ranging=True - self.clear_Irq_Status() - return self.read_range(raw_bytes=raw) - elif 'RngMasterTimeout' in irq: - print('\t\t[master] RngMasterTimeout. Resending...') - sleep(0.5) - self.set_Ranging_Params(range_addr=addr,master=True) - self.set_Tx(pBase=0x02,pBaseCount=[0xFF,0xFF]) - if monotonic() > resend: - self.set_Ranging_Params(range_addr=addr,master=True) - self.set_Tx(pBase=0x02,pBaseCount=[0xFF,0xFF]) - print('\t\t[master] resend timout') - resend=monotonic()+t_resend - print('\t\t[master] timed out') - self.get_Irq_Status(clear=[0xFF,0xFF],parse=False)[0] - self.set_Standby() - return None - - def receive_range(self,addr=[0,0,0,0],timeout=5,t_resend=3,debug=False): - timed_out = False - if not self.default_dio: - print('must set default DIO!') - return False - self.set_Ranging_Params(range_addr=addr,slave=True) - self.set_Rx(pBase=0x02,pBaseCount=[0xFF,0xFF]) # reduced pbase to 1ms - - if timeout is not None: - resend = monotonic()+t_resend - timed_out = monotonic()+timeout - # Blocking wait for interrupt on DIO - while monotonic() < timed_out: - if self.default_dio.value: - irq=self.get_Irq_Status(clear=True,parse=True)[0] - if irq: - # print('s',irq) - if debug: print(irq) - if 'RngSlaveResponseDone' in irq: - self._ranging=True - self.set_Standby() - self.clear_Irq_Status() - if debug: print('[range slave] responded to range request') - return True - elif 'RngSlaveReqDiscard' in irq: - print('\t\t[slave] RngSlaveReqDiscard. Listening again') - self.set_Ranging_Params(range_addr=addr,slave=True) - self.set_Rx(pBase=0x02,pBaseCount=[0xFF,0xFF]) - if monotonic() > resend: - self.set_Ranging_Params(range_addr=addr,slave=True) - self.set_Rx(pBase=0x02,pBaseCount=[0xFF,0xFF]) - print('\t\t[slave] receive timeout') - resend=monotonic()+t_resend - print('SLAVE timed out {}'.format(monotonic())) - irq=self.get_Irq_Status(clear=[0xFF,0xFF],parse=False) - print(irq) - self.set_Standby() - return False - - def send_fast(self,data,l): - self.txen.value=True - self.pcktparams['PayloadLength']=l - self.set_Packet_Params() - self._busywait() - with self._device as device: - device.write(b'\x1a\x00'+data,end=l+2) - self.set_Tx() - txdone=self.wait_for_irq() - self.txen.value=False - return txdone diff --git a/sx1280/sx1280.py b/sx1280/sx1280.py new file mode 100644 index 0000000..9ea05a8 --- /dev/null +++ b/sx1280/sx1280.py @@ -0,0 +1,1156 @@ +from random import random +from time import monotonic, sleep + +import digitalio +from adafruit_bus_device.spi_device import SPIDevice +from busio import SPI +from digitalio import DigitalInOut +from micropython import const + +# Radio Commands +_RADIO_GET_STATUS = const(0xC0) +_RADIO_WRITE_REGISTER = const(0x18) +_RADIO_READ_REGISTER = const(0x19) +_RADIO_WRITE_BUFFER = const(0x1A) +_RADIO_READ_BUFFER = const(0x1B) +_RADIO_SET_SLEEP = const(0x84) +_RADIO_SET_STANDBY = const(0x80) +_RADIO_SET_TX = const(0x83) +_RADIO_SET_RX = const(0x82) +_RADIO_SET_CAD = const(0xC5) +_RADIO_SET_PACKETTYPE = const(0x8A) +_RADIO_GET_PACKETTYPE = const(0x03) +_RADIO_SET_RFFREQUENCY = const(0x86) +_RADIO_SET_TXPARAMS = const(0x8E) +_RADIO_SET_CADPARAMS = const(0x88) +_RADIO_SET_BUFFERBASEADDRESS = const(0x8F) +_RADIO_SET_MODULATIONPARAMS = const(0x8B) +_RADIO_SET_PACKETPARAMS = const(0x8C) +_RADIO_GET_RXBUFFERSTATUS = const(0x17) +_RADIO_GET_PACKETSTATUS = const(0x1D) +_RADIO_GET_RSSIINST = const(0x1F) +_RADIO_SET_DIOIRQPARAMS = const(0x8D) +_RADIO_GET_IRQSTATUS = const(0x15) +_RADIO_CLR_IRQSTATUS = const(0x97) +_RADIO_SET_REGULATORMODE = const(0x96) +_RADIO_SET_AUTOFS = const(0x9E) +_RADIO_SET_RANGING_ROLE = const(0xA3) +_PACKET_TYPE_LORA = const(0x01) +_PACKET_TYPE_RANGING = const(0x02) +_PACKET_HEADER_EXPLICIT = const(0x00) # varriable length, header included +_PACKET_HEADER_IMPLICIT = const(0x80) # fixed length, no header in packet +_PACKET_CRC_MODE_ON = const(0x20) # 32 +_PACKET_CRC_MODE_OFF = const(0x00) +_PACKET_IQ_INVERT = const(0x00) +_PACKET_IQ_NORMAL = const(0x40) # 64 +_XTAL_FREQ = const(52000000) +_FREQ_STEP = _XTAL_FREQ / 262144 + +# Radio Head Constants +_RH_BROADCAST_ADDRESS = const(0xFF) +_RH_FLAGS_ACK = const(0x80) +_RH_FLAGS_RETRY = const(0x40) + +_irq1Def = ( + "RngSlaveReqDiscard", + "RngMasterResultValid", + "RngMasterTimeout", + "RngMasterReqValid", + "CadDone", + "CadDetected", + "RxTxTimeout", + "AdvRngDone", +) +_irq2Def = ( + "TxDone", + "RxDone", + "SyncWrdValid", + "SyncWrdErr", + "HeaderValid", + "HeaderErr", + "CrcErr", + "RngSlaveResponseDone", +) +# _ranging_calibrations={# SF5 SF6 SF7 SF8 SF9 SF10 +# 'BW400' :[const(10299),const(10271),const(10244),const(10242),const(10230),const(10246)], +# 'BW800' :[const(11486),const(11474),const(11453),const(11426),const(11417),const(11401)], +# 'BW1600':[const(13308),const(13493),const(13528),const(13515),const(13430),const(13376)]} +# range_Freqoff={ # SF5 SF6 SF7 SF8 SF9 SF10 +# 'BW400' :[-0.148,-0.214,-0.419,-0.853,-1.686,-3.423], +# 'BW800' :[-0.041,-0.811,-0.218,-0.429,-0.853,-1.737], +# 'BW1600':[0.103, -0.041,-0.101,-0.211,-0.424,-0.87 ]} +_mode_mask = const(0xE0) +_cmd_stat_mask = const(0x1C) + + +class SX1280: + _status = bytearray(1) + _status_msg = {"mode": "", "cmd": "", "busy": False} + _status_mode = { + 0: "N/A", + 1: "N/A", + 2: "STDBY_RC", + 3: "STDBY_XOSC", + 4: "FS", + 5: "Rx", + 6: "Tx", + } + _status_cmd = { + 0: "N/A", + 1: "Cmd Successful", + 2: "Data Available", + 3: "Timed-out", + 4: "Cmd Error", + 5: "Failure to Execute Cmd", + 6: "Tx Done", + } + _BUFFER = bytearray(10) + + pcktparams = { + "PreambleLength": 12, + "HeaderType": _PACKET_HEADER_EXPLICIT, + "PayloadLength": 0x0F, + "CrcMode": _PACKET_CRC_MODE_ON, + "InvertIQ": _PACKET_IQ_NORMAL, + } + ranging_params = {"SF": 0xA0, "BW": 0x0A, "CR": 0x01} + + def __init__( + self, + spi: SPI, + cs: DigitalInOut, + reset: DigitalInOut, + busy, + frequency, + *, + debug=False, + txen: DigitalInOut | bool = False, + rxen: DigitalInOut | bool = False, + ): + self._device = SPIDevice(spi, cs, baudrate=500000, polarity=0, phase=0) + self._reset = reset + self._reset.switch_to_input(pull=digitalio.Pull.UP) + self._busy = busy + self._busy.switch_to_input() + self.packet_type = 0 # default + self._debug = debug + self.default_dio = False + self.txen = txen + self.rxen = rxen + self.frequency = frequency + self.ranging_calibration = False + self.rng_rssi = 0 + + self.reset() + self._busywait() + self.retry_counter = 0 + self.timeouts = 0 + + # Radio Head (RH) Stuff + self.ack_delay = None + self.ack_retries = 5 + self.ack_wait = 0.2 + self.sequence_number = 0 + # self.seen_ids = bytearray(256) + + # RH Header Bytes + self.node = _RH_BROADCAST_ADDRESS + self.destination = _RH_BROADCAST_ADDRESS + self.identifier = 0 + self.flags = 0 + + # default register configuration + self.default_config() + + def default_config(self): + self.sleeping = False + self._set_ranging = False + self._ranging = False + self._status = 0 + self._autoFS = False + self._listen = False + self._range_listening = False + self.set_Standby("STDBY_RC") + self.clear_Irq_Status() + self.set_Regulator_Mode() + self.set_Packet_Type() # set to LoRa + self.frequency_ghz = self.frequency + self.set_Modulation_Params() + self.set_Packet_Params() + self.set_Buffer_Base_Address() + self.set_Tx_Param() # power=13dBm,rampTime=20us + self.high_sensitivity_lna(True) + self.set_Dio_IRQ_Params() + # Set Ranging Filter Size to 200 samples + # self._writeRegister(0x9,0x1E,20) + # Disable Advanced Ranging + self._send_command(bytes([0x9A, 0])) + # Disable long preamble + # self._send_command(bytes([0x9B,0])) + # Set Save Context + self._send_command(bytes([0xD5])) + + if self._debug: + print("Radio Initialized") + + def _convert_status(self, status): + mode = (status & _mode_mask) >> 5 + cmdstat = (status & _cmd_stat_mask) >> 2 + if mode in self._status_mode: + self._status_msg["mode"] = self._status_mode[mode] + if cmdstat in self._status_cmd: + self._status_msg["cmd"] = self._status_cmd[cmdstat] + self._status_msg["busy"] = not bool(status & 0x1) + return self._status_msg + + def _send_command(self, command, stat=False): + _size = len(command) + self._busywait() + with self._device as device: + device.write_readinto(command, self._BUFFER, out_end=_size, in_end=_size) + if stat: + print("SendCMD CMD:", [hex(i) for i in command]) + print("SendCMD BUF:", [hex(i) for i in self._BUFFER]) + if self._debug: + print("\t\t{}".format(self._convert_status(self._BUFFER[0]))) + self._busywait() + return self._BUFFER[:_size] + + def _writeRegister(self, address1, address2, data): + if self._debug: + print("Writing to:", hex(address1), hex(address2)) + self._send_command(bytes([_RADIO_WRITE_REGISTER, address1, address2, data])) + + def _readRegister(self, address1, address2): + if self._debug: + print("Reading:", hex(address1), hex(address2)) + self._busywait() + with self._device as device: + device.write(bytes([_RADIO_READ_REGISTER, address1, address2]), end=3) + device.readinto(self._BUFFER, end=2) + if self._debug: + [print(hex(i), " ", end="") for i in self._BUFFER] + print("") + self._busywait() + return self._BUFFER[1] # TODO this seems wrong + + def _readRegisters(self, address1, address2, _length=1): + """ + read_reg_cmd, addr[15:8], addr[7:0], NOP, NOP, NOP, NOP, ... + status status status status data1 data2 data3 ... + """ + _size = _length + 4 + if self._debug: + print("Reading:", hex(address1), hex(address2)) + self._busywait() + with self._device as device: + device.write_readinto( + bytes([_RADIO_READ_REGISTER, address1, address2, 0] + [0] * _length), + self._BUFFER, + out_end=_size, + in_end=_size, + ) + self._busywait() + if self._debug: + print( + "Read Regs ({}, {}) _BUFFER: {}".format( + hex(address1), hex(address2), [hex(i) for i in self._BUFFER] + ) + ) + return self._BUFFER[4:_size] + + def printRegisters(self, start=0x0900, stop=0x09FF, p=True): + _length = stop - start + 1 + _size = _length + 4 + buff = bytearray(_size) + self._busywait() + with self._device as device: + device.write_readinto( + bytes([_RADIO_READ_REGISTER]) + + int(start).to_bytes(2, "big") + + b"\x00" + + b"\x00" * _length, + buff, + out_end=_size, + in_end=_size, + ) + if p: + pass + else: + return buff + + # @timeout(3) + def _busywait(self): + _t = monotonic() + 3 + while monotonic() < _t: + if self._debug: + print("waiting for busy pin.") + if not self._busy.value: + return True + print("TIMEDOUT on busywait") + self.timeouts += 1 + if self.timeouts > 5: + self.timeouts = 0 + try: + with open("/sd/log.txt", "a") as f: + f.write("sb:{}\n".format(int(monotonic()))) + except Exception as e: + print(f"Failed to open log file {e}") + pass + self.reset_io() + if hasattr(self, "timeout_handler"): + self.timeout_handler() + return False + + def reset_io(self): + self._reset.switch_to_output(value=False) + self._busy.switch_to_input() + sleep(5) + self._reset.switch_to_input(pull=digitalio.Pull.UP) + sleep(2) + self.default_config() + + def wait_for_irq(self): + if self.default_dio: + return self.DIOwait(self.default_dio) + else: + return self.IRQwait(bit=1) + + # @timeout(3) + def DIOwait(self, pin): + _t = monotonic() + 3 + while monotonic() < _t: + if pin.value: + self.clear_Irq_Status() + return True + print("TIMEDOUT on DIOwait") + if hasattr(self, "timeout_handler"): + self.timeout_handler() + return False + + # @timeout(4) + def IRQwait(self, bit): + _t = monotonic() + 4 + while monotonic() < _t: + _irq = self.get_Irq_Status(clear=False)[1] & 1 + # print(hex(_irq)) + if (_irq >> bit) & 1: + return True + print("TIMEDOUT on IRQwait") + if hasattr(self, "timeout_handler"): + self.timeout_handler() + return False + + def set_Regulator_Mode(self, mode=0x01): + if self._debug: + print("Setting Regulator Mode") + self._send_command(bytes([_RADIO_SET_REGULATORMODE, mode])) + + def reset(self): + self._reset.switch_to_output(value=False) + sleep(0.05) # 50 ms + self._reset.switch_to_input(pull=digitalio.Pull.UP) + sleep(0.03) # 30 ms + + def set_Standby(self, state="STDBY_RC"): + if self._debug: + print("Setting Standby") + if state == "STDBY_RC": + self._send_command(bytes([_RADIO_SET_STANDBY, 0x00])) + elif state == "STDBY_XOSC": + self._send_command(bytes([_RADIO_SET_STANDBY, 0x01])) + + def sleep(self): + self._busywait() + with self._device as device: + device.write_readinto( + bytes([_RADIO_SET_SLEEP, 0x07]), self._BUFFER, out_end=2, in_end=2 + ) + self.sleeping = True + if self._debug: + print("\t\tSleeping SX1280") + print("\t\t{}".format(self._convert_status(self._BUFFER[0]))) + + def wake_up(self): + if self.sleeping: + if self._debug: + print("\t\tWaking SX1280") + with self._device as device: + sleep(0.1) + device.write(bytes([0xC0, 0])) # send no-op to wake-up + if not self._busywait(): + print("wake_up busy fail") + # reinitalize + self.default_config() + + def set_Packet_Type(self, packetType=_PACKET_TYPE_LORA): + self._packetType = packetType + if packetType == "RANGING": + self._packetType = _PACKET_TYPE_RANGING + + if self._debug: + print("Setting Packet Type") + self._send_command(bytes([_RADIO_SET_PACKETTYPE, self._packetType])) + self.packet_type = packetType + + def set_Cad_Params(self, symbol=0x80): + if self._debug: + print("Setting CAD Parameters") + self._send_command(bytes([_RADIO_SET_CADPARAMS, symbol])) + + def set_Cad(self): + if self._debug: + print("Setting CAD Search") + self._send_command(bytes([_RADIO_SET_CAD])) + self.clear_Irq_Status() + + @property + def frequency_ghz(self): + return float(str(self.frequency) + "E" + "9") + + @frequency_ghz.setter + def frequency_ghz(self, freq): + """ + 0xB89D89 = 12098953 PLL steps = 2.4 GHz + 2.4 GHz = 12098953*(52000000/(2**18)) + """ + self.frequency = freq + _f = int(float(str(freq) + "E" + "9") / _FREQ_STEP) + if self._debug: + print( + "\t\tSX1280 freq: {:G} GHz ({} PLL steps)".format( + float(str(freq) + "E" + "9"), _f + ) + ) + self._send_command( + bytes( + [_RADIO_SET_RFFREQUENCY, (_f >> 16) & 0xFF, (_f >> 8) & 0xFF, _f & 0xFF] + ) + ) + + def set_Buffer_Base_Address(self, txBaseAddress=0x00, rxBaseAddress=0x00): + if self._debug: + print("Setting Buffer Base Address") + self._txBaseAddress = txBaseAddress + self._rxBaseAddress = rxBaseAddress + self._send_command( + bytes([_RADIO_SET_BUFFERBASEADDRESS, txBaseAddress, rxBaseAddress]) + ) + + def set_Modulation_Params(self, modParam1=0x70, modParam2=0x26, modParam3=0x01): + # LoRa: modParam1=SpreadingFactor, modParam2=Bandwidth, modParam3=CodingRate + # LoRa with SF7, (BW1600=0x0A -> changed to BW400=0x26), CR 4/5 + # Must set PacketType first! - See Table 13-48,49,50 + if self._debug: + print("Setting Modulation parameters") + self._send_command( + bytes([_RADIO_SET_MODULATIONPARAMS, modParam1, modParam2, modParam3]) + ) + if self.packet_type == _PACKET_TYPE_LORA: + self._busywait() + # If the Spreading Factor selected is SF5 or SF6 + if modParam1 in (0x50, 0x60): + self._writeRegister(0x09, 0x25, 0x1E) + # If the Spreading Factor is SF7 or SF-8 + elif modParam1 in (0x70, 0x80): + self._writeRegister(0x09, 0x25, 0x37) + # If the Spreading Factor is SF9, SF10, SF11 or SF12 + elif modParam1 in (0x90, 0xA0, 0xB0, 0xC0): + self._writeRegister(0x09, 0x25, 0x32) + else: + print("Invalid Spreading Factor") + + def set_Packet_Params(self): + if self._debug: + print(self.pcktparams) + self._send_command( + bytes( + [ + _RADIO_SET_PACKETPARAMS, + self.pcktparams["PreambleLength"], + self.pcktparams["HeaderType"], + self.pcktparams["PayloadLength"], + self.pcktparams["CrcMode"], + self.pcktparams["InvertIQ"], + 0x00, + 0x00, + ] + ) + ) + + def set_Tx_Param(self, power=0x1F, rampTime=0xE0): + # power=13 dBm (0x1F), rampTime=20us (0xE0). See Table 11-47 + # P=-18+power -18+0x1F=13 + if self._debug: + print("Setting Tx Parameters") + self._send_command(bytes([_RADIO_SET_TXPARAMS, power, rampTime])) + + def write_Buffer(self, data): + # Offset will correspond to txBaseAddress in normal operation. + _offset = self._txBaseAddress + _len = len(data) + assert 0 < _len <= 252 + self._busywait() + with self._device as device: + device.write(bytes([_RADIO_WRITE_BUFFER, _offset]) + data, end=_len + 2) + + def read_Buffer(self, offset, payloadLen): + _payload = bytearray(payloadLen) + self._busywait() + with self._device as device: + device.write(bytes([_RADIO_READ_BUFFER, offset]), end=2) + device.readinto(_payload, end=payloadLen) + return _payload + + def dump_buffer(self, dbuffer): + self._busywait() + with self._device as device: + device.write(bytes([_RADIO_READ_BUFFER, 0, 0]), end=3) + device.readinto(dbuffer) + # print('Status:',self._convert_status(self._BIGBUFFER[0])) + # [print(hex(i),end=',') for i in self._BIGBUFFER[1:]] + # print('') + + def get_bw(self, _bw): + if _bw == 0x0A: + bw_hz = 1625000 + elif _bw == 0x18: + bw_hz = 812500 + elif _bw == 0x26: + bw_hz = 406250 + elif _bw == 0x34: + bw_hz = 203125 + else: + print("bad BW conversion") + return 0 + return bw_hz + + def set_Dio_IRQ_Params( + self, + irqMask=[0xFF, 0xF3], + dio1Mask=[0x00, 0x03], + dio2Mask=[0x00, 0x02], + dio3Mask=[0x40, 0x20], + ): + """ + TxDone IRQ on DIO1, RxDone IRQ on DIO2, HeaderError and RxTxTimeout IRQ on DIO3 + IRQmask (bit[0]=TxDone, bit[1]=RxDone) + 0x43: 0x23 + 0100 0011 0010 0011 + DIO1mask + 0000 0000 0000 0001 + DIO2mask + 0000 0000 0000 0010 + """ + if self._debug: + print("Setting DIO IRQ Parameters") + self._send_command( + bytes([_RADIO_SET_DIOIRQPARAMS] + irqMask + dio1Mask + dio2Mask + dio3Mask) + ) + + def clear_Irq_Status(self, val=[0xFF, 0xFF]): + if self._debug: + print("Clearing IRQ Status") + self._send_command(bytes([_RADIO_CLR_IRQSTATUS] + val)) + + def get_Irq_Status(self, clear=[0xFF, 0xFF], parse=False, debug=False): + if self._debug: + print("Getting IRQ Status") + _irq1, _irq2 = self._send_command( + bytes([_RADIO_GET_IRQSTATUS, 0x00, 0x00, 0x00]) + )[2:] + + if parse: + if self._debug: + print("IRQ[15:8]:{}, IRQ[7:0]:{}".format(hex(_irq1), hex(_irq2))) # + _rslt = [] + for i, j in zip(reversed("{:08b}".format(_irq1)), _irq1Def): # [15:8] + if int(i): + _rslt.append(j) + for i, j in zip(reversed("{:08b}".format(_irq2)), _irq2Def): # [7:0] + if int(i): + _rslt.append(j) + if debug: + print("IRQ Results: {}".format(_rslt)) + return (_rslt, hex(_irq1), hex(_irq2)) + + if clear: + # if clear == True: + clear = [0xFF, 0xFF] + self._send_command( + bytes([_RADIO_CLR_IRQSTATUS] + clear) + ) # clear IRQ status + return (_irq1, _irq2) + + def set_Tx(self, pBase=0x02, pBaseCount=[0x00, 0x00]): + # Activate transmit mode with no timeout. Tx mode will stop after first packet sent. + if self._debug: + print("Setting Tx") + # self.clear_Irq_Status([8,7]) + self.clear_Irq_Status() + self._send_command(bytes([_RADIO_SET_TX, pBase, pBaseCount[0], pBaseCount[1]])) + self._listen = False + + def set_Rx(self, pBase=0x02, pBaseCount=[0xFF, 0xFF]): + """ + pBaseCount = 16 bit parameter of how many steps to time-out + see Table 11-22 for pBase values (0xFFFF=continuous) + Time-out duration = pBase * periodBaseCount + """ + if self._debug: + print("\tSetting Rx") + # self.clear_Irq_Status([8,7]) + self.clear_Irq_Status() + self._send_command(bytes([_RADIO_SET_RX, pBase] + pBaseCount)) + + def set_autoFS(self, value): + self._send_command(bytes([_RADIO_SET_AUTOFS, bool(value)])) + self._autoFS = value + + def high_sensitivity_lna(self, enabled=True): + _reg = self._readRegister(0x8, 0x91) + if enabled: + self._writeRegister(0x8, 0x91, _reg | 0xC0) + else: + self._writeRegister(0x8, 0x91, _reg & 0x3F) + + def clear_range_samples(self): + # to clear, set bit 5 to 1 then to 0 + _reg = self._readRegister(0x9, 0x23) + # print('Register 0x923:',hex(_reg)) + _reg |= 1 << 5 + # print('Register 0x923:',hex(_reg)) + self._writeRegister(0x9, 0x23, _reg) + _reg &= ~(1 << 5) + # print('Register 0x923:',hex(_reg)) + self._writeRegister(0x9, 0x23, _reg) + + def set_Ranging_Params( + self, range_addr=[0x01, 0x02, 0x03, 0x04], master=False, slave=False + ): + self.set_Standby("STDBY_RC") + self.clear_range_samples() + self.set_Packet_Type("RANGING") + self.set_Modulation_Params( + modParam1=self.ranging_params["SF"], + modParam2=self.ranging_params["BW"], + modParam3=self.ranging_params["CR"], + ) + self.pcktparams["PreambleLength"] = 12 + self.pcktparams["PayloadLength"] = 0 + self.set_Packet_Params() + self.frequency_ghz = self.frequency + self.set_Buffer_Base_Address(txBaseAddress=0x00, rxBaseAddress=0x00) + self.set_Tx_Param() # DEFAULT:power=13dBm,rampTime=20us + if slave: + self._rangingRole = 0x00 + # Slave Ranging address + self._writeRegister(0x9, 0x19, range_addr[0]) + self._writeRegister(0x9, 0x18, range_addr[1]) + self._writeRegister(0x9, 0x17, range_addr[2]) + self._writeRegister(0x9, 0x16, range_addr[3]) + # Ranging address length + self._writeRegister(0x9, 0x31, 0x3) + # self.set_Dio_IRQ_Params(irqMask=[0x7F,0xF3],dio1Mask=[0x00,0x83],dio2Mask=[0x00,0x03],dio3Mask=[0x40,0x20]) # wrong? RangingSlaveResponseDone,RxDone,TxDone + # self.set_Dio_IRQ_Params(irqMask=[0x7F,0xF3],dio1Mask=[0x9,0x80],dio2Mask=[0x00,0x03],dio3Mask=[0x40,0x20]) # RangingMasterRequestValid,RangingSlaveRequestDiscard,RangingSlaveResponseDone + self.set_Dio_IRQ_Params( + irqMask=[0x7F, 0xF3], + dio1Mask=[0x1, 0x80], + dio2Mask=[0x00, 0x03], + dio3Mask=[0x40, 0x20], + ) # RangingSlaveRequestDiscard,RangingSlaveResponseDone + elif master: + self._rangingRole = 0x01 + # Master Ranging address + self._writeRegister(0x9, 0x15, range_addr[0]) + self._writeRegister(0x9, 0x14, range_addr[1]) + self._writeRegister(0x9, 0x13, range_addr[2]) + self._writeRegister(0x9, 0x12, range_addr[3]) + # self.set_Dio_IRQ_Params(irqMask=[0x7F,0xF3],dio1Mask=[0x7,0x80],dio2Mask=[0x00,0x01],dio3Mask=[0x00,0x00]) # wrong? RangingMasterTimeout,RangingMasterResultValid,RangingSlaveRequestDiscard,RangingSlaveResponseDone + self.set_Dio_IRQ_Params( + irqMask=[0x7F, 0xF3], + dio1Mask=[0x6, 0x00], + dio2Mask=[0x00, 0x01], + dio3Mask=[0x00, 0x00], + ) # RangingMasterTimeout,RangingMasterResultValid + + else: + print("Select Master or Slave Only") + return False + + # Set DIO IRQ Parameters + self.clear_Irq_Status() + + if self.ranging_calibration == "custom": + self.set_Ranging_Calibration(custom=self.rxtxdelay) + elif self.ranging_calibration: + self.set_Ranging_Calibration(zero=True) + else: + # Set Ranging Calibration per Section 3.3 of SemTech AN1200.29 + # TODO set based on modulation params + # self.set_Ranging_Calibration(CAL='BW1600',SF=5) + # self.set_Ranging_Calibration(CAL='BW1600',SF=9) + self.set_Ranging_Calibration(CAL="BW1600", SF=10) + + # Set Ranging Role + self._send_command(bytes([_RADIO_SET_RANGING_ROLE, self._rangingRole])) + + self.high_sensitivity_lna(True) + + self._set_ranging = True + + def stop_ranging(self): + self.set_Standby("STDBY_RC") + self.set_Packet_Type() # set to LoRa + self.set_Packet_Params() + self.high_sensitivity_lna(True) + self.set_Dio_IRQ_Params() + if self.txen: + self.txen.value = False + self.rxen.value = False + self._set_ranging = False + + def read_range(self, raw=True, raw_bytes=False): + if not self._ranging: + print("Start ranging before attempting to read") + return + + self.set_Standby("STDBY_XOSC") + # enable LoRa modem clock + _temp = self._readRegister(0x9, 0x7F) | (1 << 1) + self._writeRegister(0x9, 0x7F, _temp) + # Set the ranging type for filtered or raw + _conf = self._readRegister(0x9, 0x24) + if raw: + _conf = (_conf & 0xCF) | 0x0 + else: + # _conf = (_conf & 0xCF) | 0x10 # averaged + # _conf = (_conf & 0xCF) | 0x20 # debiased + _conf = (_conf & 0xCF) | 0x30 # filtered + # print('Range Data Type:',hex(_conf)) + self._writeRegister(0x9, 0x24, _conf) + + # Read the 24-bit value + self._rbuff = bytearray(4) + self._rbuff = self._readRegisters(0x9, 0x61, 4) # confirmed working + self.rng_rssi = -1 * self._rbuff[3] / 2 + # print('rng_rssi: {}'.format(self.rng_rssi)) + + self.set_Standby("STDBY_RC") + + if raw_bytes: + return self._rbuff[:3] + + _val = 0 | (self._rbuff[0] << 16) + _val |= self._rbuff[1] << 8 + _val |= self._rbuff[2] + + # dist in meters = _val * 150/(2^12 * BW in MHz) = 2scomp / (BW in Hz * 36621.09375) + _2scomp = ( + self.complement2(_val, 24) + / self.get_bw(self.ranging_params["BW"]) + * 36621.09375 + ) + if raw: + return _2scomp + # averaged, debiased, or filtered results + return _2scomp * 20.0 / 100.0 + + def get_Freq_Error_Indicator(self): + # Read the 20-bit value (based on StuartsProjects github implementation) + self.set_Standby("STDBY_XOSC") + efeRaw = self._readRegisters(0x9, 0x54, 3) + efeRaw[0] = efeRaw[0] & 0x0F # clear bit 20 which is always set + self.set_Standby() + return efeRaw + + def set_Ranging_Calibration(self, CAL="BW1600", SF=5, zero=False, custom=False): + if zero: + CAL = 0 + elif custom: + CAL = custom + else: + CAL = 0 + self._writeRegister(0x9, 0x2D, CAL & 0xFF) # cal[7:0] + self._writeRegister(0x9, 0x2C, (CAL >> 8) & 0xFF) # cal[15:8] + + def calc_efe(self, efeRaw): + efe = 0 | (efeRaw[0] << 16) + efe |= efeRaw[1] << 8 + efe |= efeRaw[2] + # efe &= 0x0FFFFF # now performed in get_Freq_Error_Indicator step + efeHz = ( + 1.55 + * self.complement2(efe, 20) + / (1625000 / self.get_bw(self.ranging_params["BW"])) + ) + return efeHz + + def complement2(self, num, bitCnt): + retVal = num + if retVal >= 2 << (bitCnt - 2): + retVal -= 2 << (bitCnt - 1) + return retVal + + def get_Packet_Status(self): + # See Table 11-63 + self._packet_status = [] + p_stat = self._send_command( + bytes([_RADIO_GET_PACKETSTATUS, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + ) + # [print(hex(i)+' ',end='') for i in self._BUFFER[:6]] + self.rssiSync = -1 * int(p_stat[2]) / 2 + self.snr = int(p_stat[3]) / 4 + return p_stat + + def get_Rx_Buffer_Status(self): + self._send_command(bytes([_RADIO_GET_RXBUFFERSTATUS, 0x00, 0x00, 0x00])) + return self._BUFFER[:4] + + def send( + self, + data, + pin=None, + irq=False, + header=True, + ID=0, + target=0, + action=0, + keep_listening=False, + ): + """Send a string of data using the transmitter. + You can only send 252 bytes at a time + (limited by chip's FIFO size and appended headers). + """ + return self.send_mod(data, keep_listening=keep_listening, header=header) + + @property + def packet_status(self): + self.get_Packet_Status() + return (self.rssiSync, self.snr) + + @property + def listen(self): + return self._listen + + @listen.setter + def listen(self, enable): + if enable: + if not self._listen: + if self.rxen: + self.txen.value = False + self.rxen.value = True + self.set_Rx() + self._listen = True + else: + if self.rxen: + self.rxen.value = False + self.set_Standby("STDBY_RC") + self._listen = False + + def receive(self, continuous=True, keep_listening=True): + if not self._listen: + self.listen = True + if continuous: + self._buf_status = self.get_Rx_Buffer_Status() + self._packet_len = self._buf_status[2] + self._packet_pointer = self._buf_status[3] + if self._packet_len > 0: + if self._debug: + print("Offset:", self._packet_pointer, "Length:", self._packet_len) + packet = self.read_Buffer( + offset=self._packet_pointer, payloadLen=self._packet_len + 1 + ) + if not keep_listening: + self.listen = False + return packet[1:] + + @property + def packet_info(self): + return (self._packet_len, self._packet_pointer) + + def rssi(self, raw=False): + self._rssi = self._send_command(bytes([_RADIO_GET_PACKETSTATUS, 0x00, 0x00])) + if raw: + return self._rssi[-1] + else: + return -1 * self._rssi[-1] / 2 # dBm + + def status(self, raw=False): + self._busywait() + with self._device as device: + device.write_readinto( + bytes([_RADIO_GET_STATUS]), self._BUFFER, out_end=1, in_end=1 + ) + # if raw: print([hex(i) for i in self._BUFFER]) + if raw: + return self._BUFFER[0] + self._busywait() + return self._convert_status(self._BUFFER[0]) + + def send_mod( + self, + data, + *, + keep_listening=False, + header=False, + destination=None, + node=None, + identifier=None, + flags=None, + debug=False, + ): + data_len = len(data) + assert 0 < data_len <= 252 + if self.txen: + self.rxen.value = False + self.txen.value = True + if debug: + print("\t\ttxen:on, rxen:off") + if header: + payload = bytearray(4) + if destination is None: # use attribute + payload[0] = self.destination + else: # use kwarg + payload[0] = destination + if node is None: # use attribute + payload[1] = self.node + else: # use kwarg + payload[1] = node + if identifier is None: # use attribute + payload[2] = self.identifier + else: # use kwarg + payload[2] = identifier + if flags is None: # use attribute + payload[3] = self.flags + else: # use kwarg + payload[3] = flags + if debug: + print("HEADER: {}".format([hex(i) for i in payload])) + data = payload + data + data_len += 4 + # Configure Packet Length + self.pcktparams["PayloadLength"] = data_len + self.set_Packet_Params() + self.write_Buffer(data) + self.set_Tx() + txdone = self.wait_for_irq() + if keep_listening: + self.listen = True + else: + if self.txen: + self.txen.value = False + if debug: + print("\t\ttxen:off, rxen:n/a") + return txdone + + def receive_mod( + self, + *, + keep_listening=True, + with_header=False, + with_ack=False, + timeout=0.5, + debug=False, + ): + timed_out = False + if not self.default_dio: + print("must set default DIO!") + return False + if timeout is not None: + if not self._listen: + self.listen = True + start = monotonic() + timed_out = False + # Blocking wait for interrupt on DIO + while not timed_out and not self.default_dio.value: + if (monotonic() - start) >= timeout: + timed_out = True + # Radio has received something! + packet = None + # Stop receiving other packets + self.listen = False + if not timed_out: + self._buf_status = self.get_Rx_Buffer_Status() + self._packet_len = self._buf_status[2] + self._packet_pointer = self._buf_status[3] + if self._packet_len > 0: + packet = self.read_Buffer( + offset=self._packet_pointer, payloadLen=self._packet_len + 1 + )[1:] + self.clear_Irq_Status() + if self._packet_len > 4: + if ( + self.node != _RH_BROADCAST_ADDRESS + and packet[0] != _RH_BROADCAST_ADDRESS + and packet[0] != self.node + ): + if debug: + print("Overheard packet:", packet) + packet = None + # send ACK unless this was an ACK or a broadcast + elif ( + with_ack + and ((packet[3] & _RH_FLAGS_ACK) == 0) + and (packet[0] != _RH_BROADCAST_ADDRESS) + ): + # delay before sending Ack to give receiver a chance to get ready + if self.ack_delay is not None: + sleep(self.ack_delay) + self.send_mod( + b"!", + keep_listening=keep_listening, + header=True, + destination=packet[1], + node=packet[0], + identifier=packet[2], + flags=(packet[3] | _RH_FLAGS_ACK), + ) + # print('sband ack to {}'.format(packet[1])) + if not with_header: # skip the header if not wanted + packet = packet[4:] + # Listen again if necessary and return the result packet. + if keep_listening: + self.listen = True + return packet + + def send_with_ack(self, data): + """Reliable Datagram mode: + Send a packet with data and wait for an ACK response. + The packet header is automatically generated. + If enabled, the packet transmission will be retried on failure + """ + if self.ack_retries: + retries_remaining = self.ack_retries + else: + retries_remaining = 1 + got_ack = False + self.retry_counter = 0 + self.sequence_number = (self.sequence_number + 1) & 0xFF + while not got_ack and retries_remaining: + self.identifier = self.sequence_number + self.send_mod(data, header=True, keep_listening=True) + # Don't look for ACK from Broadcast message + if self.destination == _RH_BROADCAST_ADDRESS: + print("sband destination=RHbroadcast address (dont look for ack)") + got_ack = True + else: + # wait for a packet from our destination + ack_packet = self.receive_mod(timeout=self.ack_wait, with_header=True) + if ack_packet is not None: + if ack_packet[3] & _RH_FLAGS_ACK: + # check the ID + if ack_packet[2] == self.identifier: + got_ack = True + break + else: + print( + "bad sband ack ID. Looking for: {}".format( + hex(self.identifier) + ) + ) + # delay random amount before retry + if not got_ack: + self.retry_counter += 1 + print("no sband ack, sending again...") + sleep(self.ack_wait + self.ack_wait * random()) + retries_remaining = retries_remaining - 1 + # set retry flag in packet header + self.flags |= _RH_FLAGS_RETRY + self.flags = 0 # clear flags + return got_ack + + def get_range( + self, addr=[0, 0, 0, 0], raw=False, timeout=10, t_resend=3, debug=False, delay=1 + ): + timed_out = False + irq = [] + if not self.default_dio: + print("must set default DIO!") + return False + # sleep a delayed amount to give slave time to configure + sleep(delay) + self.set_Ranging_Params(range_addr=addr, master=True) + self.set_Tx(pBase=0x02, pBaseCount=[0xFF, 0xFF]) # reduced pbase to 1ms + + if timeout is not None: + resend = monotonic() + t_resend + timed_out = monotonic() + timeout + while monotonic() < timed_out: + if self.default_dio.value: + irq = self.get_Irq_Status(clear=True, parse=True)[0] + if irq: + # print('m',irq) + if debug: + print(irq) + if "RngMasterResultValid" in irq: + self._ranging = True + self.clear_Irq_Status() + return self.read_range(raw_bytes=raw) + elif "RngMasterTimeout" in irq: + print("\t\t[master] RngMasterTimeout. Resending...") + sleep(0.5) + self.set_Ranging_Params(range_addr=addr, master=True) + self.set_Tx(pBase=0x02, pBaseCount=[0xFF, 0xFF]) + if monotonic() > resend: + self.set_Ranging_Params(range_addr=addr, master=True) + self.set_Tx(pBase=0x02, pBaseCount=[0xFF, 0xFF]) + print("\t\t[master] resend timout") + resend = monotonic() + t_resend + print("\t\t[master] timed out") + self.get_Irq_Status(clear=[0xFF, 0xFF], parse=False)[0] + self.set_Standby() + return None + + def receive_range(self, addr=[0, 0, 0, 0], timeout=5, t_resend=3, debug=False): + timed_out = False + if not self.default_dio: + print("must set default DIO!") + return False + self.set_Ranging_Params(range_addr=addr, slave=True) + self.set_Rx(pBase=0x02, pBaseCount=[0xFF, 0xFF]) # reduced pbase to 1ms + + if timeout is not None: + resend = monotonic() + t_resend + timed_out = monotonic() + timeout + # Blocking wait for interrupt on DIO + while monotonic() < timed_out: + if self.default_dio.value: + irq = self.get_Irq_Status(clear=True, parse=True)[0] + if irq: + # print('s',irq) + if debug: + print(irq) + if "RngSlaveResponseDone" in irq: + self._ranging = True + self.set_Standby() + self.clear_Irq_Status() + if debug: + print("[range slave] responded to range request") + return True + elif "RngSlaveReqDiscard" in irq: + print("\t\t[slave] RngSlaveReqDiscard. Listening again") + self.set_Ranging_Params(range_addr=addr, slave=True) + self.set_Rx(pBase=0x02, pBaseCount=[0xFF, 0xFF]) + if monotonic() > resend: + self.set_Ranging_Params(range_addr=addr, slave=True) + self.set_Rx(pBase=0x02, pBaseCount=[0xFF, 0xFF]) + print("\t\t[slave] receive timeout") + resend = monotonic() + t_resend + print("SLAVE timed out {}".format(monotonic())) + irq = self.get_Irq_Status(clear=[0xFF, 0xFF], parse=False) + print(irq) + self.set_Standby() + return False + + def send_fast(self, data, length): + self.txen.value = True + self.pcktparams["PayloadLength"] = length + self.set_Packet_Params() + self._busywait() + with self._device as device: + device.write(b"\x1a\x00" + data, end=length + 2) + self.set_Tx() + txdone = self.wait_for_irq() + self.txen.value = False + return txdone diff --git a/typings/micropython.pyi b/typings/micropython.pyi new file mode 100644 index 0000000..c5db06d --- /dev/null +++ b/typings/micropython.pyi @@ -0,0 +1,354 @@ +""" +Access and control MicroPython internals. + +MicroPython module: https://docs.micropython.org/en/v1.25.0/library/micropython.html + +--- +Module: 'micropython' on micropython-v1.25.0-rp2-RPI_PICO +--- +pysquared: Borrowed from https://github.com/Josverl/micropython-stubs +https://pypi.org/project/micropython-rp2-stubs/#files +""" + +# MCU: {'build': '', 'ver': '1.25.0', 'version': '1.25.0', 'port': 'rp2', 'board': 'RPI_PICO', 'mpy': 'v6.3', 'family': 'micropython', 'cpu': 'RP2040', 'arch': 'armv6m'} +# Stubber: v1.24.0 +from __future__ import annotations + +from typing import Any, Callable, Optional, Tuple, overload + +from _typeshed import Incomplete +from typing_extensions import ParamSpec, TypeVar + +_T = TypeVar("_T") +_F = TypeVar("_F", bound=Callable[..., Any]) +Const_T = TypeVar("Const_T", int, float, str, bytes, Tuple) +_Param = ParamSpec("_Param") +_Ret = TypeVar("_Ret") + +@overload +def opt_level() -> int: + """ + If *level* is given then this function sets the optimisation level for subsequent + compilation of scripts, and returns ``None``. Otherwise it returns the current + optimisation level. + + The optimisation level controls the following compilation features: + + - Assertions: at level 0 assertion statements are enabled and compiled into the + bytecode; at levels 1 and higher assertions are not compiled. + - Built-in ``__debug__`` variable: at level 0 this variable expands to ``True``; + at levels 1 and higher it expands to ``False``. + - Source-code line numbers: at levels 0, 1 and 2 source-code line number are + stored along with the bytecode so that exceptions can report the line number + they occurred at; at levels 3 and higher line numbers are not stored. + + The default optimisation level is usually level 0. + """ + +@overload +def opt_level(level: int, /) -> None: + """ + If *level* is given then this function sets the optimisation level for subsequent + compilation of scripts, and returns ``None``. Otherwise it returns the current + optimisation level. + + The optimisation level controls the following compilation features: + + - Assertions: at level 0 assertion statements are enabled and compiled into the + bytecode; at levels 1 and higher assertions are not compiled. + - Built-in ``__debug__`` variable: at level 0 this variable expands to ``True``; + at levels 1 and higher it expands to ``False``. + - Source-code line numbers: at levels 0, 1 and 2 source-code line number are + stored along with the bytecode so that exceptions can report the line number + they occurred at; at levels 3 and higher line numbers are not stored. + + The default optimisation level is usually level 0. + """ + +@overload +def mem_info() -> None: + """ + Print information about currently used memory. If the *verbose* argument + is given then extra information is printed. + + The information that is printed is implementation dependent, but currently + includes the amount of stack and heap used. In verbose mode it prints out + the entire heap indicating which blocks are used and which are free. + """ + +@overload +def mem_info(verbose: Any, /) -> None: + """ + Print information about currently used memory. If the *verbose* argument + is given then extra information is printed. + + The information that is printed is implementation dependent, but currently + includes the amount of stack and heap used. In verbose mode it prints out + the entire heap indicating which blocks are used and which are free. + """ + +def kbd_intr(chr: int) -> None: + """ + Set the character that will raise a `KeyboardInterrupt` exception. By + default this is set to 3 during script execution, corresponding to Ctrl-C. + Passing -1 to this function will disable capture of Ctrl-C, and passing 3 + will restore it. + + This function can be used to prevent the capturing of Ctrl-C on the + incoming stream of characters that is usually used for the REPL, in case + that stream is used for other purposes. + """ + ... + +@overload +def qstr_info() -> None: + """ + Print information about currently interned strings. If the *verbose* + argument is given then extra information is printed. + + The information that is printed is implementation dependent, but currently + includes the number of interned strings and the amount of RAM they use. In + verbose mode it prints out the names of all RAM-interned strings. + """ + +@overload +def qstr_info(verbose: bool, /) -> None: + """ + Print information about currently interned strings. If the *verbose* + argument is given then extra information is printed. + + The information that is printed is implementation dependent, but currently + includes the number of interned strings and the amount of RAM they use. In + verbose mode it prints out the names of all RAM-interned strings. + """ + +def schedule(func: Callable[[_T], None], arg: _T, /) -> None: + """ + Schedule the function *func* to be executed "very soon". The function + is passed the value *arg* as its single argument. "Very soon" means that + the MicroPython runtime will do its best to execute the function at the + earliest possible time, given that it is also trying to be efficient, and + that the following conditions hold: + + - A scheduled function will never preempt another scheduled function. + - Scheduled functions are always executed "between opcodes" which means + that all fundamental Python operations (such as appending to a list) + are guaranteed to be atomic. + - A given port may define "critical regions" within which scheduled + functions will never be executed. Functions may be scheduled within + a critical region but they will not be executed until that region + is exited. An example of a critical region is a preempting interrupt + handler (an IRQ). + + A use for this function is to schedule a callback from a preempting IRQ. + Such an IRQ puts restrictions on the code that runs in the IRQ (for example + the heap may be locked) and scheduling a function to call later will lift + those restrictions. + + On multi-threaded ports, the scheduled function's behaviour depends on + whether the Global Interpreter Lock (GIL) is enabled for the specific port: + + - If GIL is enabled, the function can preempt any thread and run in its + context. + - If GIL is disabled, the function will only preempt the main thread and run + in its context. + + Note: If `schedule()` is called from a preempting IRQ, when memory + allocation is not allowed and the callback to be passed to `schedule()` is + a bound method, passing this directly will fail. This is because creating a + reference to a bound method causes memory allocation. A solution is to + create a reference to the method in the class constructor and to pass that + reference to `schedule()`. This is discussed in detail here + :ref:`reference documentation ` under "Creation of Python + objects". + + There is a finite queue to hold the scheduled functions and `schedule()` + will raise a `RuntimeError` if the queue is full. + """ + ... + +def stack_use() -> int: + """ + Return an integer representing the current amount of stack that is being + used. The absolute value of this is not particularly useful, rather it + should be used to compute differences in stack usage at different points. + """ + ... + +def heap_unlock() -> int: + """ + Lock or unlock the heap. When locked no memory allocation can occur and a + `MemoryError` will be raised if any heap allocation is attempted. + `heap_locked()` returns a true value if the heap is currently locked. + + These functions can be nested, ie `heap_lock()` can be called multiple times + in a row and the lock-depth will increase, and then `heap_unlock()` must be + called the same number of times to make the heap available again. + + Both `heap_unlock()` and `heap_locked()` return the current lock depth + (after unlocking for the former) as a non-negative integer, with 0 meaning + the heap is not locked. + + If the REPL becomes active with the heap locked then it will be forcefully + unlocked. + + Note: `heap_locked()` is not enabled on most ports by default, + requires ``MICROPY_PY_MICROPYTHON_HEAP_LOCKED``. + """ + +def const(expr: Const_T, /) -> Const_T: + """ + Used to declare that the expression is a constant so that the compiler can + optimise it. The use of this function should be as follows:: + + from micropython import const + + CONST_X = const(123) + CONST_Y = const(2 * CONST_X + 1) + + Constants declared this way are still accessible as global variables from + outside the module they are declared in. On the other hand, if a constant + begins with an underscore then it is hidden, it is not available as a global + variable, and does not take up any memory during execution. + + This `const` function is recognised directly by the MicroPython parser and is + provided as part of the :mod:`micropython` module mainly so that scripts can be + written which run under both CPython and MicroPython, by following the above + pattern. + """ + ... + +def heap_lock() -> int: + """ + Lock or unlock the heap. When locked no memory allocation can occur and a + `MemoryError` will be raised if any heap allocation is attempted. + `heap_locked()` returns a true value if the heap is currently locked. + + These functions can be nested, ie `heap_lock()` can be called multiple times + in a row and the lock-depth will increase, and then `heap_unlock()` must be + called the same number of times to make the heap available again. + + Both `heap_unlock()` and `heap_locked()` return the current lock depth + (after unlocking for the former) as a non-negative integer, with 0 meaning + the heap is not locked. + + If the REPL becomes active with the heap locked then it will be forcefully + unlocked. + + Note: `heap_locked()` is not enabled on most ports by default, + requires ``MICROPY_PY_MICROPYTHON_HEAP_LOCKED``. + """ + +def alloc_emergency_exception_buf(size: int, /) -> None: + """ + Allocate *size* bytes of RAM for the emergency exception buffer (a good + size is around 100 bytes). The buffer is used to create exceptions in cases + when normal RAM allocation would fail (eg within an interrupt handler) and + therefore give useful traceback information in these situations. + + A good way to use this function is to put it at the start of your main script + (eg ``boot.py`` or ``main.py``) and then the emergency exception buffer will be active + for all the code following it. + """ + ... + +class RingIO: + def readinto(self, buf, nbytes: Optional[Any] = None) -> int: + """ + Read available bytes into the provided ``buf``. If ``nbytes`` is + specified then read at most that many bytes. Otherwise, read at + most ``len(buf)`` bytes. + + Return value: Integer count of the number of bytes read into ``buf``. + """ + ... + + def write(self, buf) -> int: + """ + Non-blocking write of bytes from ``buf`` into the ringbuffer, limited + by the available space in the ringbuffer. + + Return value: Integer count of bytes written. + """ + ... + + def readline(self, nbytes: Optional[Any] = None) -> bytes: + """ + Read a line, ending in a newline character or return if one exists in + the buffer, else return available bytes in buffer. If ``nbytes`` is + specified then read at most that many bytes. + + Return value: a bytes object containing the line read. + """ + ... + + def any(self) -> int: + """ + Returns an integer counting the number of characters that can be read. + """ + ... + + def read(self, nbytes: Optional[Any] = None) -> bytes: + """ + Read available characters. This is a non-blocking function. If ``nbytes`` + is specified then read at most that many bytes, otherwise read as much + data as possible. + + Return value: a bytes object containing the bytes read. Will be + zero-length bytes object if no data is available. + """ + ... + + def close(self) -> Incomplete: + """ + No-op provided as part of standard `stream` interface. Has no effect + on data in the ringbuffer. + """ + ... + + def __init__(self, size) -> None: ... + +# decorators +@overload # force merge +def viper(_func: Callable[_Param, _Ret], /) -> Callable[_Param, _Ret]: + """ + The Viper code emitter is not fully compliant. It supports special Viper native data types in pursuit of performance. + Integer processing is non-compliant because it uses machine words: arithmetic on 32 bit hardware is performed modulo 2**32. + Like the Native emitter Viper produces machine instructions but further optimisations are performed, substantially increasing + performance especially for integer arithmetic and bit manipulations. + See: https://docs.micropython.org/en/latest/reference/speed_python.html?highlight=viper#the-native-code-emitter + """ + ... + +@overload # force merge +def native(_func: Callable[_Param, _Ret], /) -> Callable[_Param, _Ret]: + """ + This causes the MicroPython compiler to emit native CPU opcodes rather than bytecode. + It covers the bulk of the MicroPython functionality, so most functions will require no adaptation. + See: https://docs.micropython.org/en/latest/reference/speed_python.html#the-native-code-emitter + """ + ... + +@overload # force merge +def asm_thumb(_func: Callable[_Param, _Ret], /) -> Callable[_Param, _Ret]: + """ + This decorator is used to mark a function as containing inline assembler code. + The assembler code is written is a subset of the ARM Thumb-2 instruction set, and is executed on the target CPU. + + Availability: Only on specific boards where MICROPY_EMIT_INLINE_THUMB is defined. + See: https://docs.micropython.org/en/latest/reference/asm_thumb2_index.html + """ + ... + +@overload # force merge +def asm_xtensa(_func: Callable[_Param, _Ret], /) -> Callable[_Param, _Ret]: + """ + This decorator is used to mark a function as containing inline assembler code for the esp8266. + The assembler code is written in the Xtensa instruction set, and is executed on the target CPU. + + Availability: Only on eps8266 boards. + """ + ... + # See : + # - https://github.com/orgs/micropython/discussions/12965 + # - https://github.com/micropython/micropython/pull/16731 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..0fecb60 --- /dev/null +++ b/uv.lock @@ -0,0 +1,310 @@ +version = 1 +requires-python = ">=3.13" + +[[package]] +name = "adafruit-blinka" +version = "8.56.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adafruit-circuitpython-typing" }, + { name = "adafruit-platformdetect" }, + { name = "adafruit-pureio" }, + { name = "binho-host-adapter" }, + { name = "pyftdi" }, + { name = "sysv-ipc", marker = "platform_machine != 'mips' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/01/b9a729272389db23e8397d4d5da0bbc2a87c73483098958022d00bd13c7f/adafruit_blinka-8.56.0.tar.gz", hash = "sha256:f119cf100bc2e33ec5bc90ab6552bf19a7a6f362c0d51ad2dd819716dab2b798", size = 257328 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/7d/60a3e48cbae3b5d3c552af84f04cbcfd925c718f0736c2bd751e306e2f9b/adafruit_blinka-8.56.0-py3-none-any.whl", hash = "sha256:2e0e1b2e031915a9daea25fdc9fe31be5f458667f5ca835948d5928193342afc", size = 375266 }, +] + +[[package]] +name = "adafruit-circuitpython-busdevice" +version = "5.2.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adafruit-blinka" }, + { name = "adafruit-circuitpython-typing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/f6347ab32f077413c20f55bc4b0f1592f35affd4d26753394c5ed6c36c4c/adafruit_circuitpython_busdevice-5.2.11.tar.gz", hash = "sha256:a9a1310bee7021703ccc247bb3ff04d0873573948a6c7bee9016361cd6707a71", size = 27627 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/c7/9f0e2b2674cb5b1fb35d067a7585a2a76596a36044264eb390980d428ccf/adafruit_circuitpython_busdevice-5.2.11-py3-none-any.whl", hash = "sha256:d4379c9ae86a15f7044dea815a94525ca9eda6a7c0b2fa0e75cf9e700c9384b8", size = 7539 }, +] + +[[package]] +name = "adafruit-circuitpython-connectionmanager" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adafruit-blinka" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/8b/8316002905f97a7f7e9c3a53dd9bb5a17889033ec55c403a6e55077f6298/adafruit_circuitpython_connectionmanager-3.1.3.tar.gz", hash = "sha256:0f133bdedf454ede0c0a866ed605fe166cc85f75cfcea74758e3622ae403e5f9", size = 37381 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/7d/896b31bd31eff89e5cab5d3acec9d3d34f5a0654ceab25e01865e628d9f9/adafruit_circuitpython_connectionmanager-3.1.3-py3-none-any.whl", hash = "sha256:9df3a4c617dae27bad1ac8607f1a084312c8498d831ebe1c6a2c8d5cb309daea", size = 7811 }, +] + +[[package]] +name = "adafruit-circuitpython-requests" +version = "4.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adafruit-blinka" }, + { name = "adafruit-circuitpython-connectionmanager" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/16/11d311192a8ec4d0b6370fb36226a95c641a993cd6fe6986fd13e6ac3ea5/adafruit_circuitpython_requests-4.1.10.tar.gz", hash = "sha256:a8657fe4b3a74c224e7365ebefa7742cc198373c1702bfc3f2b0ca3c4fd288b3", size = 66262 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d1/f83b60b9283db4075cb30e5938cee3f3b795b5d558619fd37d635cf8abb2/adafruit_circuitpython_requests-4.1.10-py3-none-any.whl", hash = "sha256:6a54aa39436e83e0dc75d60c861e5befa333bff265b947ea263156e64e467122", size = 10732 }, +] + +[[package]] +name = "adafruit-circuitpython-typing" +version = "1.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "adafruit-blinka" }, + { name = "adafruit-circuitpython-busdevice" }, + { name = "adafruit-circuitpython-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/80/8c280fa7d42a23dce40b2fe64f708d18fa32b384adbf6934955d2c2ebecf/adafruit_circuitpython_typing-1.11.2.tar.gz", hash = "sha256:c7ac8532a9ad7e4a65d5588764b7483c0b6967d305c37faebcc0c5356d677e33", size = 29277 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/d5/76a6bca9cf08907b48dfc8ccbccbd190155353f521876e02d6b7bb244003/adafruit_circuitpython_typing-1.11.2-py3-none-any.whl", hash = "sha256:e1401a09bbfdf67e43875cc6755b3af0eda8381b12c9c8f759bd7676b7425e1c", size = 11101 }, +] + +[[package]] +name = "adafruit-platformdetect" +version = "3.77.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/4e/2b2ca031227de47e2aab6cf092b78934c9c0033a685075ddab3e0c0b55fe/adafruit_platformdetect-3.77.0.tar.gz", hash = "sha256:adce6386059637e92b4cb5d3430d016119cd3eb19f9276920c54515f3d798949", size = 48024 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/18/b18e9ff2aee42f03082675c5d18d4eb02411477e07c86d74833d3396792e/Adafruit_PlatformDetect-3.77.0-py3-none-any.whl", hash = "sha256:93f599c21e7db2d92bc32ac69ba5063876404c4af87d11358863e62f407409be", size = 25542 }, +] + +[[package]] +name = "adafruit-pureio" +version = "1.1.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/b7/f1672435116822079bbdab42163f9e6424769b7db778873d95d18c085230/Adafruit_PureIO-1.1.11.tar.gz", hash = "sha256:c4cfbb365731942d1f1092a116f47dfdae0aef18c5b27f1072b5824ad5ea8c7c", size = 35511 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/9d/28e9d12f36e13c5f2acba3098187b0e931290ecd1d8df924391b5ad2db19/Adafruit_PureIO-1.1.11-py3-none-any.whl", hash = "sha256:281ab2099372cc0decc26326918996cbf21b8eed694ec4764d51eefa029d324e", size = 10678 }, +] + +[[package]] +name = "binho-host-adapter" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyserial" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/36/29b7b896e83e195fac6d64ccff95c0f24a18ee86e7437a22e60e0331d90a/binho-host-adapter-0.1.6.tar.gz", hash = "sha256:1e6da7a84e208c13b5f489066f05774bff1d593d0f5bf1ca149c2b8e83eae856", size = 10068 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/6b/0f13486003aea3eb349c2946b7ec9753e7558b78e35d22c938062a96959c/binho_host_adapter-0.1.6-py3-none-any.whl", hash = "sha256:f71ca176c1e2fc1a5dce128beb286da217555c6c7c805f2ed282a6f3507ec277", size = 10540 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "circuitpy-flight-software" +version = "2.0.0" +source = { virtual = "." } +dependencies = [ + { name = "adafruit-circuitpython-typing" }, + { name = "circuitpython-stubs" }, + { name = "pre-commit" }, + { name = "pyright", extra = ["nodejs"] }, +] + +[package.metadata] +requires-dist = [ + { name = "adafruit-circuitpython-typing", specifier = "==1.11.2" }, + { name = "circuitpython-stubs", specifier = "==9.2.5" }, + { name = "pre-commit", specifier = "==4.0.1" }, + { name = "pyright", extras = ["nodejs"], specifier = "==1.1.399" }, +] + +[[package]] +name = "circuitpython-stubs" +version = "9.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/d7/ce0a51ba9f6b15bdde6a79cfa96f3c63117dd749465be18980461771cfa9/circuitpython_stubs-9.2.5.tar.gz", hash = "sha256:2b2be4172552bdb9c5a1e9923124ee62f0bed4c0b128d862ad1baf8866e67174", size = 348760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/27/72e6715132a325db32ff06475c7da1a217e65082d710b1a7e8ce3c36ca0e/circuitpython_stubs-9.2.5-py3-none-any.whl", hash = "sha256:9b2e8ba04e7fee6a0155e5915b36a1a911e2e3486c795669baa9c2db76102a7c", size = 1006110 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, +] + +[[package]] +name = "identify" +version = "2.6.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "nodejs-wheel-binaries" +version = "22.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/5b/6c5f973765b96793d4e4d03684bcbd273b17e471ecc7e9bec4c32b595ebd/nodejs_wheel_binaries-22.15.0.tar.gz", hash = "sha256:ff81aa2a79db279c2266686ebcb829b6634d049a5a49fc7dc6921e4f18af9703", size = 8054 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/a8/a32e5bb99e95c536e7dac781cffab1e7e9f8661b8ee296b93df77e4df7f9/nodejs_wheel_binaries-22.15.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:aa16366d48487fff89446fb237693e777aa2ecd987208db7d4e35acc40c3e1b1", size = 50514526 }, + { url = "https://files.pythonhosted.org/packages/05/e8/eb024dbb3a7d3b98c8922d1c306be989befad4d2132292954cb902f43b07/nodejs_wheel_binaries-22.15.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:a54bb3fee9170003fa8abc69572d819b2b1540344eff78505fcc2129a9175596", size = 51409179 }, + { url = "https://files.pythonhosted.org/packages/3f/0f/baa968456c3577e45c7d0e3715258bd175dcecc67b683a41a5044d5dae40/nodejs_wheel_binaries-22.15.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:867121ccf99d10523f6878a26db86e162c4939690e24cfb5bea56d01ea696c93", size = 57364460 }, + { url = "https://files.pythonhosted.org/packages/2f/a2/977f63cd07ed8fc27bc0d0cd72e801fc3691ffc8cd40a51496ff18a6d0a2/nodejs_wheel_binaries-22.15.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab0fbcda2ddc8aab7db1505d72cb958f99324b3834c4543541a305e02bfe860", size = 57889101 }, + { url = "https://files.pythonhosted.org/packages/67/7f/57b9c24a4f0d25490527b043146aa0fdff2d8fdc82f90667cdaf6f00cfc9/nodejs_wheel_binaries-22.15.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2bde1d8e00cd955b9ce9ee9ac08309923e2778a790ee791b715e93e487e74bfd", size = 59190817 }, + { url = "https://files.pythonhosted.org/packages/fd/7f/970acbe33b81c22b3c7928f52e32347030aa46d23d779cf781cf9a9cf557/nodejs_wheel_binaries-22.15.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:acdd4ef73b6701aab9fbe02ac5e104f208a5e3c300402fa41ad7bc7f49499fbf", size = 60220316 }, + { url = "https://files.pythonhosted.org/packages/07/4c/030243c04bb60f0de66c2d7ee3be289c6d28ef09113c06ffa417bdfedf8f/nodejs_wheel_binaries-22.15.0-py2.py3-none-win_amd64.whl", hash = "sha256:51deaf13ee474e39684ce8c066dfe86240edb94e7241950ca789befbbbcbd23d", size = 40718853 }, + { url = "https://files.pythonhosted.org/packages/1f/49/011d472814af4fabeaab7d7ce3d5a1a635a3dadc23ae404d1f546839ecb3/nodejs_wheel_binaries-22.15.0-py2.py3-none-win_arm64.whl", hash = "sha256:01a3fe4d60477f93bf21a44219db33548c75d7fed6dc6e6f4c05cf0adf015609", size = 36436645 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, +] + +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + +[[package]] +name = "pyftdi" +version = "0.56.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyserial" }, + { name = "pyusb" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/96/a8de7b7e5556d4b00d1ca1969fc34c89a1b6d177876c7a31d42631b090fc/pyftdi-0.56.0-py3-none-any.whl", hash = "sha256:3ef0baadbf9031dde9d623ae66fac2d16ded36ce1b66c17765ca1944cb38b8b0", size = 145718 }, +] + +[[package]] +name = "pyright" +version = "1.1.399" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584 }, +] + +[package.optional-dependencies] +nodejs = [ + { name = "nodejs-wheel-binaries" }, +] + +[[package]] +name = "pyserial" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585 }, +] + +[[package]] +name = "pyusb" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/6b/ce3727395e52b7b76dfcf0c665e37d223b680b9becc60710d4bc08b7b7cb/pyusb-1.3.1.tar.gz", hash = "sha256:3af070b607467c1c164f49d5b0caabe8ac78dbed9298d703a8dbf9df4052d17e", size = 77281 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/b8/27e6312e86408a44fe16bd28ee12dd98608b39f7e7e57884a24e8f29b573/pyusb-1.3.1-py3-none-any.whl", hash = "sha256:bf9b754557af4717fe80c2b07cc2b923a9151f5c08d17bdb5345dac09d6a0430", size = 58465 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "sysv-ipc" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/d7/5d2f861155e9749f981e6c58f2a482d3ab458bf8c35ae24d4b4d5899ebf9/sysv_ipc-1.1.0.tar.gz", hash = "sha256:0f063cbd36ec232032e425769ebc871f195a7d183b9af32f9901589ea7129ac3", size = 99448 } + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + +[[package]] +name = "virtualenv" +version = "20.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/e0/633e369b91bbc664df47dcb5454b6c7cf441e8f5b9d0c250ce9f0546401e/virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", size = 4346945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6", size = 4329461 }, +]