Skip to content

Commit 2f7eec1

Browse files
authored
Zigpy packet capture interface (#54)
* Zigpy packet capture interface * Include `CHANNELS_LIST` * Rename `-h` to `-p` * Rename `randomize` to `--channel-hop-randomly` * Use the simplified interface * Add a new command for concurrent packet capture * Bump bellows * Fix CI * Add `requirements_test.txt` * Update code folder in CI
1 parent e13497f commit 2f7eec1

File tree

8 files changed

+225
-222
lines changed

8 files changed

+225
-222
lines changed

.github/workflows/ci.yml

+10-201
Original file line numberDiff line numberDiff line change
@@ -1,208 +1,17 @@
11
name: CI
22

3-
# yamllint disable-line rule:truthy
43
on:
54
push:
65
pull_request: ~
76

8-
env:
9-
CACHE_VERSION: 1
10-
DEFAULT_PYTHON: 3.8
11-
PRE_COMMIT_HOME: ~/.cache/pre-commit
12-
137
jobs:
14-
# Separate job to pre-populate the base dependency cache
15-
# This prevent upcoming jobs to do the same individually
16-
prepare-base:
17-
name: Prepare base dependencies
18-
runs-on: ubuntu-latest
19-
strategy:
20-
matrix:
21-
python-version: [3.8, 3.9, "3.10", "3.11"]
22-
steps:
23-
- name: Check out code from GitHub
24-
uses: actions/checkout@v3
25-
- name: Set up Python ${{ matrix.python-version }}
26-
id: python
27-
uses: actions/setup-python@v4
28-
with:
29-
python-version: ${{ matrix.python-version }}
30-
- name: Restore base Python virtual environment
31-
id: cache-venv
32-
uses: actions/cache@v3
33-
with:
34-
path: venv
35-
key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}
36-
restore-keys: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-
37-
- name: Create Python virtual environment
38-
if: steps.cache-venv.outputs.cache-hit != 'true'
39-
run: |
40-
python -m venv venv
41-
. venv/bin/activate
42-
pip install -U pip setuptools pre-commit
43-
pip install -e '.[testing]'
44-
45-
pre-commit:
46-
name: Prepare pre-commit environment
47-
runs-on: ubuntu-latest
48-
needs: prepare-base
49-
steps:
50-
- name: Check out code from GitHub
51-
uses: actions/checkout@v3
52-
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
53-
uses: actions/setup-python@v4
54-
id: python
55-
with:
56-
python-version: ${{ env.DEFAULT_PYTHON }}
57-
- name: Restore base Python virtual environment
58-
id: cache-venv
59-
uses: actions/cache@v3
60-
with:
61-
path: venv
62-
key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}
63-
- name: Fail job if Python cache restore failed
64-
if: steps.cache-venv.outputs.cache-hit != 'true'
65-
run: |
66-
echo "Failed to restore Python virtual environment from cache"
67-
exit 1
68-
- name: Restore pre-commit environment from cache
69-
id: cache-precommit
70-
uses: actions/cache@v3
71-
with:
72-
path: ${{ env.PRE_COMMIT_HOME }}
73-
key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
74-
restore-keys: ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-
75-
- name: Install pre-commit dependencies
76-
if: steps.cache-precommit.outputs.cache-hit != 'true'
77-
run: |
78-
. venv/bin/activate
79-
pre-commit install-hooks
80-
81-
lint-pre-commit:
82-
name: Check pre-commit
83-
runs-on: ubuntu-latest
84-
needs: pre-commit
85-
steps:
86-
- name: Check out code from GitHub
87-
uses: actions/checkout@v3
88-
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
89-
uses: actions/setup-python@v4
90-
id: python
91-
with:
92-
python-version: ${{ env.DEFAULT_PYTHON }}
93-
- name: Restore base Python virtual environment
94-
id: cache-venv
95-
uses: actions/cache@v3
96-
with:
97-
path: venv
98-
key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}
99-
- name: Fail job if Python cache restore failed
100-
if: steps.cache-venv.outputs.cache-hit != 'true'
101-
run: |
102-
echo "Failed to restore Python virtual environment from cache"
103-
exit 1
104-
- name: Restore pre-commit environment from cache
105-
id: cache-precommit
106-
uses: actions/cache@v3
107-
with:
108-
path: ${{ env.PRE_COMMIT_HOME }}
109-
key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
110-
- name: Fail job if cache restore failed
111-
if: steps.cache-venv.outputs.cache-hit != 'true'
112-
run: |
113-
echo "Failed to restore Python virtual environment from cache"
114-
exit 1
115-
- name: Run pre-commit
116-
run: |
117-
. venv/bin/activate
118-
pre-commit run --all-files --show-diff-on-failure
119-
120-
pytest:
121-
runs-on: ubuntu-latest
122-
needs: prepare-base
123-
strategy:
124-
matrix:
125-
python-version: [3.8, 3.9, "3.10", "3.11"]
126-
name: >-
127-
Run tests Python ${{ matrix.python-version }}
128-
steps:
129-
- name: Check out code from GitHub
130-
uses: actions/checkout@v3
131-
- name: Set up Python ${{ matrix.python-version }}
132-
uses: actions/setup-python@v4
133-
id: python
134-
with:
135-
python-version: ${{ matrix.python-version }}
136-
- name: Restore base Python virtual environment
137-
id: cache-venv
138-
uses: actions/cache@v3
139-
with:
140-
path: venv
141-
key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}
142-
- name: Fail job if Python cache restore failed
143-
if: steps.cache-venv.outputs.cache-hit != 'true'
144-
run: |
145-
echo "Failed to restore Python virtual environment from cache"
146-
exit 1
147-
- name: Register Python problem matcher
148-
run: |
149-
echo "::add-matcher::.github/workflows/matchers/python.json"
150-
- name: Install Pytest Annotation plugin
151-
run: |
152-
. venv/bin/activate
153-
# Ideally this should be part of our dependencies
154-
# However this plugin is fairly new and doesn't run correctly
155-
# on a non-GitHub environment.
156-
pip install pytest-github-actions-annotate-failures
157-
- name: Run pytest
158-
run: |
159-
. venv/bin/activate
160-
pytest \
161-
-qq \
162-
--timeout=20 \
163-
--durations=10 \
164-
--cov zigpy_cli \
165-
--cov-config pyproject.toml \
166-
-o console_output_style=count \
167-
-p no:sugar \
168-
tests
169-
- name: Upload coverage artifact
170-
uses: actions/upload-artifact@v3
171-
with:
172-
name: coverage-${{ matrix.python-version }}
173-
path: .coverage
174-
175-
176-
coverage:
177-
name: Process test coverage
178-
runs-on: ubuntu-latest
179-
needs: pytest
180-
steps:
181-
- name: Check out code from GitHub
182-
uses: actions/checkout@v3
183-
- name: Set up Python ${{ matrix.python-version }}
184-
uses: actions/setup-python@v4
185-
id: python
186-
with:
187-
python-version: ${{ env.DEFAULT_PYTHON }}
188-
- name: Restore base Python virtual environment
189-
id: cache-venv
190-
uses: actions/cache@v3
191-
with:
192-
path: venv
193-
key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }}
194-
- name: Fail job if Python cache restore failed
195-
if: steps.cache-venv.outputs.cache-hit != 'true'
196-
run: |
197-
echo "Failed to restore Python virtual environment from cache"
198-
exit 1
199-
- name: Download all coverage artifacts
200-
uses: actions/download-artifact@v3
201-
- name: Combine coverage results
202-
run: |
203-
. venv/bin/activate
204-
coverage combine coverage*/.coverage*
205-
coverage report
206-
coverage xml
207-
- name: Upload coverage to Codecov
208-
uses: codecov/codecov-action@v3
8+
shared-ci:
9+
uses: zigpy/workflows/.github/workflows/ci.yml@main
10+
with:
11+
CODE_FOLDER: zigpy_cli
12+
CACHE_VERSION: 3
13+
PRE_COMMIT_CACHE_PATH: ~/.cache/pre-commit
14+
PYTHON_VERSION_DEFAULT: 3.9.15
15+
MINIMUM_COVERAGE_PERCENTAGE: 1
16+
secrets:
17+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

.github/workflows/publish-to-pypi.yml

+6-20
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,12 @@
1-
name: Publish distributions to PyPI and TestPyPI
1+
name: Publish distributions to PyPI
2+
23
on:
34
release:
45
types:
56
- published
67

78
jobs:
8-
build-and-publish:
9-
name: Build and publish distributions to PyPI and TestPyPI
10-
runs-on: ubuntu-latest
11-
steps:
12-
- uses: actions/checkout@v3
13-
- name: Set up Python 3.8
14-
uses: actions/setup-python@v4
15-
with:
16-
python-version: 3.8
17-
- name: Install wheel
18-
run: >-
19-
pip install wheel build
20-
- name: Build
21-
run: >-
22-
python3 -m build
23-
- name: Publish distribution to PyPI
24-
uses: pypa/gh-action-pypi-publish@release/v1
25-
with:
26-
password: ${{ secrets.PYPI_API_TOKEN }}
9+
shared-build-and-publish:
10+
uses: zigpy/workflows/.github/workflows/publish-to-pypi.yml@main
11+
secrets:
12+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies = [
1818
"coloredlogs",
1919
"scapy",
2020
"zigpy>=0.55.0",
21-
"bellows>=0.35.1",
21+
"bellows>=0.43.0",
2222
"zigpy-deconz>=0.21.0",
2323
"zigpy-xbee>=0.18.0",
2424
"zigpy-zboss>=1.1.0",

requirements_test.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
coverage[toml]
2+
pytest
3+
pytest-asyncio
4+
pytest-cov
5+
pytest-timeout

zigpy_cli/common.py

+15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import click
2+
from zigpy.types import Channels
23

34

45
class HexOrDecIntParamType(click.ParamType):
@@ -17,4 +18,18 @@ def convert(self, value, param, ctx):
1718
self.fail(f"{value!r} is not a valid integer", param, ctx)
1819

1920

21+
class ChannelsType(click.ParamType):
22+
name = "channels"
23+
24+
def convert(self, value, param, ctx):
25+
if isinstance(value, Channels):
26+
return value
27+
28+
try:
29+
return Channels.from_channel_list(map(int, value.split(",")))
30+
except ValueError:
31+
self.fail(f"{value!r} is not a valid channel list", param, ctx)
32+
33+
2034
HEX_OR_DEC_INT = HexOrDecIntParamType()
35+
CHANNELS_LIST = ChannelsType()

zigpy_cli/helpers.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import struct
2+
3+
import zigpy.types as t
4+
5+
6+
class PcapWriter:
7+
"""Class responsible to write in PCAP format."""
8+
9+
def __init__(self, file):
10+
"""Initialize PCAP file and write global header."""
11+
self.file = file
12+
13+
def write_header(self):
14+
self.file.write(
15+
struct.pack("<L", 0xA1B2C3D4)
16+
+ struct.pack("<H", 2)
17+
+ struct.pack("<H", 4)
18+
+ struct.pack("<L", 0)
19+
+ struct.pack("<L", 0)
20+
+ struct.pack("<L", 65535)
21+
+ struct.pack("<L", 283) # LINKTYPE_IEEE802_15_4_TAP
22+
)
23+
24+
def write_packet(self, packet: t.CapturedPacket) -> None:
25+
"""Write a packet with its header and TLV metadata."""
26+
timestamp_sec = int(packet.timestamp.timestamp())
27+
timestamp_usec = int(packet.timestamp.microsecond)
28+
29+
sub_tlvs = b""
30+
31+
# RSSI
32+
sub_tlvs += (
33+
t.uint16_t(1).serialize()
34+
+ t.uint16_t(4).serialize()
35+
+ t.Single(packet.rssi).serialize()
36+
)
37+
38+
# LQI
39+
sub_tlvs += (
40+
t.uint16_t(10).serialize()
41+
+ t.uint16_t(1).serialize()
42+
+ t.uint8_t(packet.lqi).serialize()
43+
+ b"\x00\x00\x00"
44+
)
45+
46+
# Channel Assignment
47+
sub_tlvs += (
48+
t.uint16_t(3).serialize()
49+
+ t.uint16_t(3).serialize()
50+
+ t.uint16_t(packet.channel).serialize()
51+
+ t.uint8_t(0).serialize() # page 0
52+
+ b"\x00"
53+
)
54+
55+
# FCS type
56+
sub_tlvs += (
57+
t.uint16_t(0).serialize()
58+
+ t.uint16_t(1).serialize()
59+
+ t.uint8_t(1).serialize() # FCS type 1
60+
+ b"\x00\x00\x00"
61+
)
62+
63+
tlvs = b""
64+
65+
# TAP header: version:u8, reserved: u8, length: u16
66+
tlvs += struct.pack("<BBH", 0, 0, 4 + len(sub_tlvs))
67+
assert len(sub_tlvs) % 4 == 0
68+
69+
data = tlvs + sub_tlvs + packet.data + packet.compute_fcs()
70+
71+
self.file.write(
72+
struct.pack("<L", timestamp_sec)
73+
+ struct.pack("<L", timestamp_usec)
74+
+ struct.pack("<L", len(data))
75+
+ struct.pack("<L", len(data))
76+
+ data
77+
)

0 commit comments

Comments
 (0)