mirror of
https://github.com/halleysfifthinc/AVCLAN-Mockingboard.git
synced 2026-08-07 01:13:18 +00:00
Switch to a python based [streaming] packet capture setup
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,212 +0,0 @@
|
|||||||
# This file is machine-generated - editing it directly is not advised
|
|
||||||
|
|
||||||
julia_version = "1.12.0"
|
|
||||||
manifest_format = "2.0"
|
|
||||||
project_hash = "eb09ad96f566b90998c9665d6eb0aaba02e61d92"
|
|
||||||
|
|
||||||
[[deps.AVCLANPipe]]
|
|
||||||
deps = ["Dates", "LibSerialPort", "PcapTools", "UnixTimes"]
|
|
||||||
path = "."
|
|
||||||
uuid = "bfacebfd-1bd5-49e7-81bd-5edd8f4074de"
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[[deps.ArgTools]]
|
|
||||||
uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
|
|
||||||
version = "1.1.2"
|
|
||||||
|
|
||||||
[[deps.Artifacts]]
|
|
||||||
uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.Base64]]
|
|
||||||
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.Dates]]
|
|
||||||
deps = ["Printf"]
|
|
||||||
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.Downloads]]
|
|
||||||
deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"]
|
|
||||||
uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
|
|
||||||
version = "1.6.0"
|
|
||||||
|
|
||||||
[[deps.FileWatching]]
|
|
||||||
uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.JLLWrappers]]
|
|
||||||
deps = ["Artifacts", "Preferences"]
|
|
||||||
git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2"
|
|
||||||
uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
|
|
||||||
version = "1.7.1"
|
|
||||||
|
|
||||||
[[deps.JuliaSyntaxHighlighting]]
|
|
||||||
deps = ["StyledStrings"]
|
|
||||||
uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011"
|
|
||||||
version = "1.12.0"
|
|
||||||
|
|
||||||
[[deps.LibCURL]]
|
|
||||||
deps = ["LibCURL_jll", "MozillaCACerts_jll"]
|
|
||||||
uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"
|
|
||||||
version = "0.6.4"
|
|
||||||
|
|
||||||
[[deps.LibCURL_jll]]
|
|
||||||
deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"]
|
|
||||||
uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"
|
|
||||||
version = "8.11.1+1"
|
|
||||||
|
|
||||||
[[deps.LibGit2]]
|
|
||||||
deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"]
|
|
||||||
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.LibGit2_jll]]
|
|
||||||
deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"]
|
|
||||||
uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5"
|
|
||||||
version = "1.9.0+0"
|
|
||||||
|
|
||||||
[[deps.LibSSH2_jll]]
|
|
||||||
deps = ["Artifacts", "Libdl", "OpenSSL_jll"]
|
|
||||||
uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"
|
|
||||||
version = "1.11.3+1"
|
|
||||||
|
|
||||||
[[deps.LibSerialPort]]
|
|
||||||
deps = ["Libdl", "libserialport_jll"]
|
|
||||||
git-tree-sha1 = "69403284a6217f6e60e76553032a103893b7d910"
|
|
||||||
uuid = "a05a14c7-6e3b-5ba9-90a2-45558833e1df"
|
|
||||||
version = "0.5.2"
|
|
||||||
|
|
||||||
[[deps.Libdl]]
|
|
||||||
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.Logging]]
|
|
||||||
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.Markdown]]
|
|
||||||
deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"]
|
|
||||||
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.Mmap]]
|
|
||||||
uuid = "a63ad114-7e13-5084-954f-fe012c677804"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.MozillaCACerts_jll]]
|
|
||||||
uuid = "14a3606d-f60d-562e-9121-12d972cd8159"
|
|
||||||
version = "2025.5.20"
|
|
||||||
|
|
||||||
[[deps.NetworkOptions]]
|
|
||||||
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
|
|
||||||
version = "1.3.0"
|
|
||||||
|
|
||||||
[[deps.OpenSSL_jll]]
|
|
||||||
deps = ["Artifacts", "Libdl"]
|
|
||||||
uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95"
|
|
||||||
version = "3.5.1+0"
|
|
||||||
|
|
||||||
[[deps.PcapTools]]
|
|
||||||
deps = ["Dates", "Mmap", "UnixTimes", "UnsafeArrays"]
|
|
||||||
path = "dev/PcapTools"
|
|
||||||
uuid = "222fe7e8-3f39-464a-bf97-d9bbb753f246"
|
|
||||||
version = "1.1.1"
|
|
||||||
|
|
||||||
[[deps.Pkg]]
|
|
||||||
deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"]
|
|
||||||
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
|
|
||||||
version = "1.12.0"
|
|
||||||
|
|
||||||
[deps.Pkg.extensions]
|
|
||||||
REPLExt = "REPL"
|
|
||||||
|
|
||||||
[deps.Pkg.weakdeps]
|
|
||||||
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
|
|
||||||
|
|
||||||
[[deps.Preferences]]
|
|
||||||
deps = ["TOML"]
|
|
||||||
git-tree-sha1 = "0f27480397253da18fe2c12a4ba4eb9eb208bf3d"
|
|
||||||
uuid = "21216c6a-2e73-6563-6e65-726566657250"
|
|
||||||
version = "1.5.0"
|
|
||||||
|
|
||||||
[[deps.Printf]]
|
|
||||||
deps = ["Unicode"]
|
|
||||||
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.Random]]
|
|
||||||
deps = ["SHA"]
|
|
||||||
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.SHA]]
|
|
||||||
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
|
|
||||||
version = "0.7.0"
|
|
||||||
|
|
||||||
[[deps.StyledStrings]]
|
|
||||||
uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.TOML]]
|
|
||||||
deps = ["Dates"]
|
|
||||||
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
|
|
||||||
version = "1.0.3"
|
|
||||||
|
|
||||||
[[deps.Tar]]
|
|
||||||
deps = ["ArgTools", "SHA"]
|
|
||||||
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
|
|
||||||
version = "1.10.0"
|
|
||||||
|
|
||||||
[[deps.UUIDs]]
|
|
||||||
deps = ["Random", "SHA"]
|
|
||||||
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.Unicode]]
|
|
||||||
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
|
|
||||||
version = "1.11.0"
|
|
||||||
|
|
||||||
[[deps.UnixTimes]]
|
|
||||||
deps = ["Dates"]
|
|
||||||
git-tree-sha1 = "1add5a01e8b2f45371d96583595ab2bf734a49e3"
|
|
||||||
uuid = "ab1a18e7-b408-4913-896c-624bb82ed7f4"
|
|
||||||
version = "1.7.2"
|
|
||||||
|
|
||||||
[deps.UnixTimes.extensions]
|
|
||||||
UnixTimesMakieExt = ["Makie", "Observables"]
|
|
||||||
UnixTimesStructTypesExt = "StructTypes"
|
|
||||||
UnixTimesTimeZonesExt = "TimeZones"
|
|
||||||
|
|
||||||
[deps.UnixTimes.weakdeps]
|
|
||||||
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
|
|
||||||
Observables = "510215fc-4207-5dde-b226-833fc4488ee2"
|
|
||||||
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
|
|
||||||
TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"
|
|
||||||
|
|
||||||
[[deps.UnsafeArrays]]
|
|
||||||
git-tree-sha1 = "efcbb6828697ffffc245a9de7300552535abe005"
|
|
||||||
uuid = "c4a57d5a-5b31-53a6-b365-19f8c011fbd6"
|
|
||||||
version = "1.0.8"
|
|
||||||
|
|
||||||
[[deps.Zlib_jll]]
|
|
||||||
deps = ["Libdl"]
|
|
||||||
uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
|
|
||||||
version = "1.3.1+2"
|
|
||||||
|
|
||||||
[[deps.libserialport_jll]]
|
|
||||||
deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
|
|
||||||
git-tree-sha1 = "aacc5af8e8ba0b48a6feadac7afd4bca5d4e98d8"
|
|
||||||
uuid = "220460dc-b50e-5ed0-8176-09b0fd261e90"
|
|
||||||
version = "0.1.3+0"
|
|
||||||
|
|
||||||
[[deps.nghttp2_jll]]
|
|
||||||
deps = ["Artifacts", "Libdl"]
|
|
||||||
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"
|
|
||||||
version = "1.64.0+1"
|
|
||||||
|
|
||||||
[[deps.p7zip_jll]]
|
|
||||||
deps = ["Artifacts", "Libdl"]
|
|
||||||
uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
|
|
||||||
version = "17.5.0+2"
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
name = "AVCLANPipe"
|
|
||||||
uuid = "bfacebfd-1bd5-49e7-81bd-5edd8f4074de"
|
|
||||||
authors = ["Allen Hill <allenofthehills@gmail.com>"]
|
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
|
|
||||||
LibSerialPort = "a05a14c7-6e3b-5ba9-90a2-45558833e1df"
|
|
||||||
PcapTools = "222fe7e8-3f39-464a-bf97-d9bbb753f246"
|
|
||||||
UnixTimes = "ab1a18e7-b408-4913-896c-624bb82ed7f4"
|
|
||||||
@@ -5,7 +5,32 @@
|
|||||||
- Copy or link `avclan_plugin.lua` to the Wireshark Lua Pluginds folder
|
- Copy or link `avclan_plugin.lua` to the Wireshark Lua Pluginds folder
|
||||||
- Linking is more convenient if modifying/developing the dissector
|
- Linking is more convenient if modifying/developing the dissector
|
||||||
`ln -s $(pwd)/avclan_plugin.lua ~/.local/lib/wireshark/plugins/avclan_plugin.lua`
|
`ln -s $(pwd)/avclan_plugin.lua ~/.local/lib/wireshark/plugins/avclan_plugin.lua`
|
||||||
- Install Julia and needed packages (I recommend using [juliaup](https://github.com/JuliaLang/juliaup) or an official binary from the Julialang website. Binaries from your distribution are typically out of date and/or built incorrectly.)
|
- The bridge script ([extcap/avclan-pcap.py](extcap/avclan-pcap.py)) needs Python 3 and, for live capture only, [pyserial](https://pypi.org/project/pyserial/). Install pyserial so the `python3` that Wireshark runs can import it (e.g. `pip install pyserial`, or your distro's `python3-serial`). The `--convert` mode and the Wireshark interface listing work without it. (Alternatively, run the script via [uv](https://docs.astral.sh/uv/) — `uv run --script extcap/avclan-pcap.py …` — which resolves the inline PEP 723 dependency automatically; but the GUI extcap uses plain `python3`, so the install above is what makes capture work there.)
|
||||||
- From this folder, start Julia with the local project environment using `julia --project=@.`
|
|
||||||
- Run `] instantiate` to install the necessary Julia packages
|
## Live capture (Wireshark GUI)
|
||||||
- Run `julia src/pipepackets.jl | wireshark -k -i -` to pipe packets logged by the Mockingboard over serial into Wireshark. If the Lua dissector is installed correctly, the packets should be correctly recognized and dissected as IEBUS/AVCLAN packets
|
|
||||||
|
The easiest way to stream packets is as an [extcap](https://www.wireshark.org/docs/man-pages/extcap.html) interface, which appears right in Wireshark's capture list:
|
||||||
|
|
||||||
|
- Make the script executable and link it into Wireshark's **Personal Extcap path** (Help > About Wireshark, Folders tab — alongside the Lua plugin folder above):
|
||||||
|
|
||||||
|
chmod +x extcap/avclan-pcap.py
|
||||||
|
mkdir ~/.local/lib/wireshark/extcap
|
||||||
|
ln -s "$(pwd)/extcap/avclan-pcap.py" ~/.local/lib/wireshark/extcap/avclan-pcap.py
|
||||||
|
|
||||||
|
- Restart Wireshark. **AVC-LAN (serial)** now shows up in the interface list. The script autodetects a USB-UART adapter; to override, open the interface's gear/options and set the serial port. Click the shark fin to start. Wireshark owns the process, so stopping the capture (or unplugging the adapter) shuts the bridge down cleanly.
|
||||||
|
|
||||||
|
## Live capture (command line)
|
||||||
|
|
||||||
|
If you prefer the pipe, the same script works as a plain producer:
|
||||||
|
|
||||||
|
./extcap/avclan-pcap.py --port /dev/ttyUSB0 | wireshark -k -i -
|
||||||
|
|
||||||
|
Omit `--port` (or pass `--port auto`) to autodetect the adapter.
|
||||||
|
|
||||||
|
## Converting a text log to pcap
|
||||||
|
|
||||||
|
The Mockingboard's text-mode REPL output (e.g. [msgdumps/myfile.txt](msgdumps/myfile.txt)) can be turned into a capture file offline:
|
||||||
|
|
||||||
|
./extcap/avclan-pcap.py --convert msgdumps/myfile.txt -o out.pcap
|
||||||
|
|
||||||
|
Use a `.pcapng` extension to get pcapng instead (requires `editcap`, which ships with Wireshark). With the Lua dissector installed, the result is recognized and dissected as IEBUS/AVCLAN packets.
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
Copyright (c) 2020 Christian Rorvik <christian.rorvik@gmail.com>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
name = "PcapTools"
|
|
||||||
uuid = "222fe7e8-3f39-464a-bf97-d9bbb753f246"
|
|
||||||
authors = ["Christian Rorvik <christian.rorvik@gmail.com>"]
|
|
||||||
version = "1.1.1"
|
|
||||||
|
|
||||||
[deps]
|
|
||||||
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
|
|
||||||
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
|
|
||||||
UnixTimes = "ab1a18e7-b408-4913-896c-624bb82ed7f4"
|
|
||||||
UnsafeArrays = "c4a57d5a-5b31-53a6-b365-19f8c011fbd6"
|
|
||||||
|
|
||||||
[compat]
|
|
||||||
UnixTimes = "1"
|
|
||||||
UnsafeArrays = "1"
|
|
||||||
julia = "1.4"
|
|
||||||
|
|
||||||
[extras]
|
|
||||||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
|
||||||
|
|
||||||
[targets]
|
|
||||||
test = ["Test"]
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# PcapTools
|
|
||||||
|
|
||||||
[](https://travis-ci.com/ancapdev/PcapTools.jl)
|
|
||||||
|
|
||||||
[](https://coveralls.io/github/ancapdev/PcapTools.jl)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
module PcapTools
|
|
||||||
|
|
||||||
using Dates
|
|
||||||
using Mmap
|
|
||||||
using UnixTimes
|
|
||||||
using UnsafeArrays
|
|
||||||
|
|
||||||
export PcapHeader, RecordHeader
|
|
||||||
export PcapRecord
|
|
||||||
export PcapReader, PcapStreamReader, PcapBufferReader
|
|
||||||
export PcapWriter, PcapStreamWriter
|
|
||||||
export LINKTYPE_NULL, LINKTYPE_ETHERNET
|
|
||||||
export splitcap
|
|
||||||
|
|
||||||
abstract type PcapReader end
|
|
||||||
abstract type PcapWriter end
|
|
||||||
|
|
||||||
include("pcap_header.jl")
|
|
||||||
include("record_header.jl")
|
|
||||||
include("record.jl")
|
|
||||||
include("buffer_reader.jl")
|
|
||||||
include("stream_reader.jl")
|
|
||||||
include("stream_writer.jl")
|
|
||||||
include("splitcap.jl")
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
"""
|
|
||||||
Reads pcap data from an array of bytes.
|
|
||||||
"""
|
|
||||||
mutable struct PcapBufferReader <: PcapReader
|
|
||||||
data::Vector{UInt8}
|
|
||||||
raw_header::Vector{UInt8}
|
|
||||||
header::PcapHeader
|
|
||||||
offset::Int64
|
|
||||||
mark::Int64
|
|
||||||
usec_mul::Int64
|
|
||||||
bswapped::Bool
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
PcapBufferReader(data::Vector{UInt8})
|
|
||||||
|
|
||||||
Create reader over `data`. Will read and process pcap header,
|
|
||||||
and yield records through `read(::PcapBufferReader)`.
|
|
||||||
"""
|
|
||||||
function PcapBufferReader(data::Vector{UInt8})
|
|
||||||
length(data) < sizeof(PcapHeader) && throw(EOFError())
|
|
||||||
rh = data[1:sizeof(PcapHeader)]
|
|
||||||
h = unsafe_load(Ptr{PcapHeader}(pointer(data)))
|
|
||||||
h, bswapped, nanotime = process_header(h)
|
|
||||||
new(data, rh, h, sizeof(h), -1, nanotime ? 1 : 1000, bswapped)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
PcapBufferReader(path::AbstractString)
|
|
||||||
|
|
||||||
Memory map file in `path` and create PcapBufferReader over its content.
|
|
||||||
"""
|
|
||||||
function PcapBufferReader(path::AbstractString)
|
|
||||||
io = open(path)
|
|
||||||
data = Mmap.mmap(io)
|
|
||||||
PcapBufferReader(data)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Base.close(x::PcapBufferReader)
|
|
||||||
x.data = UInt8[]
|
|
||||||
x.offset = 0
|
|
||||||
nothing
|
|
||||||
end
|
|
||||||
|
|
||||||
Base.length(x::PcapBufferReader) = length(x.data)
|
|
||||||
Base.position(x::PcapBufferReader) = x.offset
|
|
||||||
Base.seek(x::PcapBufferReader, pos) = x.offset = pos; nothing
|
|
||||||
|
|
||||||
function Base.mark(x::PcapBufferReader)
|
|
||||||
x.mark = x.offset
|
|
||||||
x.mark
|
|
||||||
end
|
|
||||||
|
|
||||||
function Base.unmark(x::PcapBufferReader)
|
|
||||||
if x.mark >= 0
|
|
||||||
x.mark = -1
|
|
||||||
true
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Base.ismarked(x::PcapBufferReader) = x.mark >= 0
|
|
||||||
|
|
||||||
function Base.reset(x::PcapBufferReader)
|
|
||||||
!ismarked(x) && error("PcapBufferReader not marked")
|
|
||||||
x.offset = x.mark
|
|
||||||
x.mark = -1
|
|
||||||
x.offset
|
|
||||||
end
|
|
||||||
|
|
||||||
Base.eof(x::PcapBufferReader) = (length(x) - x.offset) < sizeof(RecordHeader)
|
|
||||||
|
|
||||||
"""
|
|
||||||
read(x::PcapBufferReader) -> PcapRecord
|
|
||||||
|
|
||||||
Read one record from pcap data.
|
|
||||||
Throws `EOFError` if no more data available.
|
|
||||||
"""
|
|
||||||
@inline function Base.read(x::PcapBufferReader)
|
|
||||||
eof(x) && throw(EOFError())
|
|
||||||
record_offset = x.offset
|
|
||||||
p = pointer(x.data) + record_offset
|
|
||||||
GC.@preserve x begin
|
|
||||||
h = unsafe_load(Ptr{RecordHeader}(p))
|
|
||||||
end
|
|
||||||
if x.bswapped
|
|
||||||
h = bswap(h)
|
|
||||||
end
|
|
||||||
t1 = (h.ts_sec + x.header.thiszone) * 1_000_000_000
|
|
||||||
t2 = Int64(h.ts_usec) * x.usec_mul
|
|
||||||
t = UnixTime(Dates.UTInstant(Nanosecond(t1 + t2)))
|
|
||||||
x.offset += sizeof(RecordHeader) + h.incl_len
|
|
||||||
x.offset > length(x) && error("Insufficient data in pcap record")
|
|
||||||
PcapRecord(h, t, x.data, record_offset)
|
|
||||||
end
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# NOTE: Not using @enum because it craps out when displaying unknown values
|
|
||||||
const LINKTYPE_NULL = UInt32(0)
|
|
||||||
const LINKTYPE_ETHERNET = UInt32(1)
|
|
||||||
|
|
||||||
struct PcapHeader
|
|
||||||
magic::UInt32
|
|
||||||
version_major::UInt16
|
|
||||||
version_minor::UInt16
|
|
||||||
thiszone::Int32
|
|
||||||
sigfigs::UInt32
|
|
||||||
snaplen::UInt32
|
|
||||||
linktype::UInt32
|
|
||||||
end
|
|
||||||
|
|
||||||
function Base.bswap(x::PcapHeader)
|
|
||||||
PcapHeader(
|
|
||||||
bswap(x.magic),
|
|
||||||
bswap(x.version_major),
|
|
||||||
bswap(x.version_minor),
|
|
||||||
bswap(x.thiszone),
|
|
||||||
bswap(x.sigfigs),
|
|
||||||
bswap(x.snaplen),
|
|
||||||
bswap(x.linktype))
|
|
||||||
end
|
|
||||||
|
|
||||||
function process_header(x::PcapHeader)
|
|
||||||
if x.magic == 0xa1b2c3d4
|
|
||||||
bswapped = false
|
|
||||||
nanotime = false
|
|
||||||
elseif x.magic == 0xd4c3b2a1
|
|
||||||
bswapped = true
|
|
||||||
nanotime = false
|
|
||||||
elseif x.magic == 0xa1b23c4d
|
|
||||||
bswapped = false
|
|
||||||
nanotime = true
|
|
||||||
elseif x.magic == 0x4d3cb2a1
|
|
||||||
bswapped = true
|
|
||||||
nanotime = true
|
|
||||||
else
|
|
||||||
throw(ArgumentError("Invalid pcap header"))
|
|
||||||
end
|
|
||||||
if bswapped
|
|
||||||
x = bswap(x)
|
|
||||||
end
|
|
||||||
x, bswapped, nanotime
|
|
||||||
end
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"""
|
|
||||||
Record of pcap data
|
|
||||||
"""
|
|
||||||
struct PcapRecord
|
|
||||||
header::RecordHeader
|
|
||||||
timestamp::UnixTime
|
|
||||||
underlying_data::Vector{UInt8}
|
|
||||||
record_offset::Int
|
|
||||||
end
|
|
||||||
|
|
||||||
@inline function record_field_(x::PcapRecord, ::Val{:data})
|
|
||||||
offset = getfield(x, :record_offset) + sizeof(RecordHeader)
|
|
||||||
len = Int(getfield(x, :header).incl_len)
|
|
||||||
UnsafeArray{UInt8, 1}(pointer(getfield(x, :underlying_data)) + offset, (len,))
|
|
||||||
end
|
|
||||||
|
|
||||||
@inline function record_field_(x::PcapRecord, ::Val{:raw})
|
|
||||||
offset = getfield(x, :record_offset)
|
|
||||||
len = sizeof(RecordHeader) + getfield(x, :header).incl_len
|
|
||||||
UnsafeArray{UInt8, 1}(pointer(getfield(x, :underlying_data)) + offset, (len,))
|
|
||||||
end
|
|
||||||
|
|
||||||
@inline record_field_(x::PcapRecord, ::Val{f}) where {f} = getfield(x, f)
|
|
||||||
|
|
||||||
@inline Base.getproperty(x::PcapRecord, f::Symbol) = record_field_(x, Val(f))
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
struct RecordHeader
|
|
||||||
ts_sec::UInt32
|
|
||||||
ts_usec::UInt32
|
|
||||||
incl_len::UInt32
|
|
||||||
orig_len::UInt32
|
|
||||||
end
|
|
||||||
|
|
||||||
function Base.bswap(x::RecordHeader)
|
|
||||||
RecordHeader(
|
|
||||||
bswap(x.ts_sec),
|
|
||||||
bswap(x.ts_usec),
|
|
||||||
bswap(x.incl_len),
|
|
||||||
bswap(x.orig_len))
|
|
||||||
end
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
strip_nothing_(::Type{Union{Nothing, T}}) where T = T
|
|
||||||
strip_nothing_(::Type{T}) where T = T
|
|
||||||
|
|
||||||
progress_noop_(n) = nothing
|
|
||||||
|
|
||||||
mutable struct SplitCapOutput{S}
|
|
||||||
work_buffer::Vector{UInt8}
|
|
||||||
complete_buffers::Channel{Vector{UInt8}}
|
|
||||||
stream::S
|
|
||||||
end
|
|
||||||
|
|
||||||
# Since Julia (as of 1.5) doesn't support task migration,
|
|
||||||
# continue with new task after each buffer write, to rebalance across threads
|
|
||||||
function write_one_and_continue_(output::SplitCapOutput, free_buffers::Channel, pending::Threads.Atomic{Int})
|
|
||||||
i = iterate(output.complete_buffers)
|
|
||||||
if i === nothing
|
|
||||||
Threads.atomic_sub!(pending, 1)
|
|
||||||
return nothing
|
|
||||||
end
|
|
||||||
b, _ = i
|
|
||||||
write(output.stream, b)
|
|
||||||
empty!(b)
|
|
||||||
put!(free_buffers, b)
|
|
||||||
Threads.@spawn write_one_and_continue_($output, $free_buffers, $pending)
|
|
||||||
end
|
|
||||||
|
|
||||||
function splitcap(
|
|
||||||
::Type{KeyType},
|
|
||||||
::Type{StreamType},
|
|
||||||
reader::PcapReader,
|
|
||||||
record2key,
|
|
||||||
key2stream,
|
|
||||||
progress_callback = progress_noop_;
|
|
||||||
own_streams::Bool = true
|
|
||||||
) where {KeyType, StreamType}
|
|
||||||
buffer_size = 1024 * 1024 * 2
|
|
||||||
max_pending_buffers = 4
|
|
||||||
outputs = Dict{KeyType, SplitCapOutput{StreamType}}()
|
|
||||||
free_buffers = Channel{Vector{UInt8}}(Inf)
|
|
||||||
n = 0
|
|
||||||
pending = Threads.Atomic{Int}(0)
|
|
||||||
try
|
|
||||||
while !eof(reader)
|
|
||||||
record = read(reader)
|
|
||||||
dst = record2key(record)
|
|
||||||
if dst isa KeyType
|
|
||||||
output = get!(outputs, dst) do
|
|
||||||
stream = key2stream(dst)
|
|
||||||
buffer = sizehint!(UInt8[], buffer_size + 1500)
|
|
||||||
own_streams && append!(buffer, reader.raw_header)
|
|
||||||
output = SplitCapOutput{StreamType}(
|
|
||||||
buffer,
|
|
||||||
Channel{Vector{UInt8}}(max_pending_buffers),
|
|
||||||
stream)
|
|
||||||
Threads.atomic_add!(pending, 1)
|
|
||||||
Threads.@spawn write_one_and_continue_($output, $free_buffers, $pending)
|
|
||||||
output
|
|
||||||
end
|
|
||||||
append!(output.work_buffer, record.raw)
|
|
||||||
if length(output.work_buffer) >= buffer_size
|
|
||||||
put!(output.complete_buffers, output.work_buffer)
|
|
||||||
if isready(free_buffers)
|
|
||||||
output.work_buffer = take!(free_buffers)
|
|
||||||
else
|
|
||||||
output.work_buffer = sizehint!(UInt8[], buffer_size + 1500)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
n += 1
|
|
||||||
progress_callback(n)
|
|
||||||
GC.safepoint()
|
|
||||||
end
|
|
||||||
for output in values(outputs)
|
|
||||||
if !isempty(output.work_buffer)
|
|
||||||
put!(output.complete_buffers, output.work_buffer)
|
|
||||||
end
|
|
||||||
close(output.complete_buffers)
|
|
||||||
end
|
|
||||||
while pending[] != 0
|
|
||||||
sleep(0.1)
|
|
||||||
end
|
|
||||||
finally
|
|
||||||
if own_streams
|
|
||||||
for output in values(outputs)
|
|
||||||
close(output.stream)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
nothing
|
|
||||||
end
|
|
||||||
|
|
||||||
function splitcap(
|
|
||||||
reader::PcapReader,
|
|
||||||
record2key,
|
|
||||||
key2stream,
|
|
||||||
progress_callback = progress_noop_;
|
|
||||||
kwargs...
|
|
||||||
)
|
|
||||||
KeyType = strip_nothing_(Core.Compiler.return_type(record2key, Tuple{PcapRecord}))
|
|
||||||
StreamType = Core.Compiler.return_type(key2stream, Tuple{KeyType})
|
|
||||||
splitcap(KeyType, StreamType, reader, record2key, key2stream, progress_callback; kwargs...)
|
|
||||||
end
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
"""
|
|
||||||
Reads pcap data from a stream.
|
|
||||||
"""
|
|
||||||
mutable struct PcapStreamReader{Src <: IO} <: PcapReader
|
|
||||||
src::Src
|
|
||||||
raw_header::Vector{UInt8}
|
|
||||||
header::PcapHeader
|
|
||||||
usec_mul::Int64
|
|
||||||
bswapped::Bool
|
|
||||||
record_buffer::Vector{UInt8}
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
PcapStreamReader(src::IO)
|
|
||||||
|
|
||||||
Create reader over `src`. Will read and process pcap header,
|
|
||||||
and yield records through `read(::PcapStreamReader)`.
|
|
||||||
"""
|
|
||||||
function PcapStreamReader(src::Src) where {Src <: IO}
|
|
||||||
raw_header = read(src, sizeof(PcapHeader))
|
|
||||||
length(raw_header) != sizeof(PcapHeader) && throw(EOFError())
|
|
||||||
h = GC.@preserve raw_header unsafe_load(Ptr{PcapHeader}(pointer(raw_header)))
|
|
||||||
header, bswapped, nanotime = process_header(h)
|
|
||||||
new{Src}(src, raw_header, header, nanotime ? 1 : 1000, bswapped, zeros(UInt8, 9000 + sizeof(RecordHeader)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
PcapStreamReader(path)
|
|
||||||
|
|
||||||
Open file at `path` and create PcapStreamReader over its content.
|
|
||||||
"""
|
|
||||||
PcapStreamReader(path::AbstractString) = PcapStreamReader(open(path))
|
|
||||||
|
|
||||||
Base.close(x::PcapStreamReader) = close(x.src)
|
|
||||||
Base.position(x::PcapStreamReader) = position(x.src)
|
|
||||||
Base.seek(x::PcapStreamReader, pos) = seek(x.src, pos)
|
|
||||||
Base.mark(x::PcapStreamReader) = mark(x.src)
|
|
||||||
Base.unmark(x::PcapStreamReader) = unmark(x.src)
|
|
||||||
Base.ismarked(x::PcapStreamReader) = ismarked(x.src)
|
|
||||||
Base.reset(x::PcapStreamReader) = reset(x.src)
|
|
||||||
Base.eof(x::PcapStreamReader) = eof(x.src)
|
|
||||||
|
|
||||||
"""
|
|
||||||
read(x::PcapStreamReader) -> PcapRecord
|
|
||||||
|
|
||||||
Read one record from pcap data. Record is valid until next read().
|
|
||||||
Throws `EOFError` if no more data available.
|
|
||||||
"""
|
|
||||||
function Base.read(x::PcapStreamReader)
|
|
||||||
p = pointer(x.record_buffer)
|
|
||||||
GC.@preserve x begin
|
|
||||||
unsafe_read(x.src, p, sizeof(RecordHeader))
|
|
||||||
h = unsafe_load(Ptr{RecordHeader}(p))
|
|
||||||
if x.bswapped
|
|
||||||
h = bswap(h)
|
|
||||||
end
|
|
||||||
unsafe_read(x.src, p + sizeof(RecordHeader), h.incl_len)
|
|
||||||
end
|
|
||||||
t1 = (h.ts_sec + x.header.thiszone) * 1_000_000_000
|
|
||||||
t2 = Int64(h.ts_usec) * x.usec_mul
|
|
||||||
t = UnixTime(Dates.UTInstant(Nanosecond(t1 + t2)))
|
|
||||||
PcapRecord(h, t, x.record_buffer, 0)
|
|
||||||
end
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
struct PcapStreamWriter{Dst <: IO} <: PcapWriter
|
|
||||||
dst::Dst
|
|
||||||
|
|
||||||
function PcapStreamWriter{Dst}(dst::Dst; thiszone = 0, snaplen = 65535, linktype = LINKTYPE_ETHERNET) where {Dst <: IO}
|
|
||||||
h = PcapHeader(
|
|
||||||
0xa1b23c4d,
|
|
||||||
0x0002,
|
|
||||||
0x0004,
|
|
||||||
thiszone,
|
|
||||||
0,
|
|
||||||
snaplen,
|
|
||||||
linktype)
|
|
||||||
write(dst, reinterpret(UInt8, [h]))
|
|
||||||
new(dst)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PcapStreamWriter(io::IO; kwargs...) = PcapStreamWriter{typeof(io)}(io; kwargs...)
|
|
||||||
PcapStreamWriter(path::AbstractString; kwargs...) = PcapStreamWriter(open(path, "w"); kwargs...)
|
|
||||||
|
|
||||||
Base.close(x::PcapStreamWriter) = close(x.dst)
|
|
||||||
|
|
||||||
function Base.write(x::PcapStreamWriter, timestamp::UnixTime, data)
|
|
||||||
sec, nsec = fldmod(Dates.value(timestamp), 1_000_000_000)
|
|
||||||
data_length = length(data)
|
|
||||||
h = RecordHeader(sec, nsec, data_length, data_length)
|
|
||||||
write(x.dst, reinterpret(UInt8, [h]))
|
|
||||||
write(x.dst, collect(data))
|
|
||||||
nothing
|
|
||||||
end
|
|
||||||
+456
@@ -0,0 +1,456 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# /// script
|
||||||
|
# requires-python = ">=3.9"
|
||||||
|
# dependencies = ["pyserial"]
|
||||||
|
# ///
|
||||||
|
"""AVC-LAN Mockingboard -> pcap bridge for Wireshark.
|
||||||
|
|
||||||
|
Three modes, one script:
|
||||||
|
|
||||||
|
* extcap - invoked by Wireshark; appears as the "AVC-LAN (serial)" capture
|
||||||
|
interface. Wireshark owns the process lifecycle, so stopping the
|
||||||
|
capture (or unplugging the adapter) tears everything down cleanly.
|
||||||
|
* pipe - run by hand: `./avclan-pcap.py --port /dev/ttyUSB0 | wireshark -k -i -`
|
||||||
|
* convert - turn a text-mode serial log into a pcap file offline:
|
||||||
|
`./avclan-pcap.py --convert msgdumps/myfile.txt -o out.pcap`
|
||||||
|
|
||||||
|
The firmware emits frames over serial at 1.2 Mbaud, 8N1, in one of two formats
|
||||||
|
(selected at the REPL). This script understands both and emits pcap with
|
||||||
|
linktype 162 (USER15), matching the avclan_plugin.lua dissector.
|
||||||
|
|
||||||
|
Discovery (the extcap query phases), `--convert`, and argument handling run on
|
||||||
|
the standard library alone, so the script keeps a plain `python3` shebang and
|
||||||
|
Wireshark can always exec it -- Wireshark launches extcaps with a minimal PATH,
|
||||||
|
which is why a `uv` shebang here would make the interface vanish from the GUI.
|
||||||
|
The one dependency, `pyserial`, is only needed to actually read the serial port:
|
||||||
|
capture fails with a clear message if it is missing. Install it normally (so the
|
||||||
|
`python3` invocation Wireshark uses can find it), or run the script via
|
||||||
|
`uv run --script` to have uv resolve the inline dependency above.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import struct
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# pcap on-the-wire format. Mirrors the old Julia PcapTools output exactly:
|
||||||
|
# nanosecond magic, snaplen 64, linktype 162 (DLT_USER15 / wtap.USER15).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
PCAP_MAGIC_NS = 0xA1B23C4D
|
||||||
|
SNAPLEN = 64
|
||||||
|
LINKTYPE_USER15 = 162
|
||||||
|
|
||||||
|
PCAP_GLOBAL_HEADER = struct.pack(
|
||||||
|
"<IHHiIII",
|
||||||
|
PCAP_MAGIC_NS, # magic (nanosecond resolution, little-endian)
|
||||||
|
2, # version major
|
||||||
|
4, # version minor
|
||||||
|
0, # thiszone
|
||||||
|
0, # sigfigs
|
||||||
|
SNAPLEN, # snaplen
|
||||||
|
LINKTYPE_USER15,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Serial framing bytes emitted by AVCLAN_printframe(..., binary=1).
|
||||||
|
DLE = 0x10 # start of a binary frame
|
||||||
|
ETB = 0x17 # end of a binary frame (followed by \r\n)
|
||||||
|
MAX_DATA_LEN = 32 # AVCLAN payload cap; longer "length" => bad framing
|
||||||
|
|
||||||
|
BAUD = 1_200_000
|
||||||
|
|
||||||
|
# USB-UART bridge vendor IDs worth preferring during autodetect.
|
||||||
|
PREFERRED_VIDS = {
|
||||||
|
0x0403, # FTDI
|
||||||
|
0x10C4, # Silicon Labs CP210x
|
||||||
|
0x1A86, # WCH CH340/CH341
|
||||||
|
0x067B, # Prolific PL2303
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# pcap writing
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def write_global_header(out):
|
||||||
|
out.write(PCAP_GLOBAL_HEADER)
|
||||||
|
out.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def write_record(out, ts_ns, payload):
|
||||||
|
sec, nsec = divmod(ts_ns, 1_000_000_000)
|
||||||
|
out.write(struct.pack("<IIII", sec, nsec, len(payload), len(payload)))
|
||||||
|
out.write(payload)
|
||||||
|
out.flush()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Binary frame extraction (live capture / pipe modes)
|
||||||
|
#
|
||||||
|
# Length-based rather than line-based: the firmware does not escape payload
|
||||||
|
# bytes, so a data byte of 0x0A/0x10/0x17 must not be allowed to split a frame.
|
||||||
|
# We locate a DLE, read the fixed 7-byte header, trust its `length` field, then
|
||||||
|
# require an ETB exactly where it should be -- resyncing on any mismatch.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def extract_frames(buf):
|
||||||
|
"""Pull complete frame payloads out of `buf`, consuming what it returns.
|
||||||
|
|
||||||
|
Yields the dissector-ready payload for each frame:
|
||||||
|
[is_unicast, ctrl_hi, ctrl_lo, periph_hi, periph_lo, control, length, data...]
|
||||||
|
Leaves any trailing partial frame in `buf` for the next call.
|
||||||
|
"""
|
||||||
|
frames = []
|
||||||
|
pos = 0
|
||||||
|
while True:
|
||||||
|
dle = buf.find(DLE, pos)
|
||||||
|
if dle == -1:
|
||||||
|
buf.clear()
|
||||||
|
return frames
|
||||||
|
# Need DLE + 7 header bytes (the last of which is `length`).
|
||||||
|
if len(buf) < dle + 8:
|
||||||
|
del buf[:dle]
|
||||||
|
return frames
|
||||||
|
length = buf[dle + 7]
|
||||||
|
if length > MAX_DATA_LEN:
|
||||||
|
pos = dle + 1 # spurious DLE; resync past it
|
||||||
|
continue
|
||||||
|
etb = dle + 8 + length # index where ETB must sit
|
||||||
|
if len(buf) < etb + 1:
|
||||||
|
del buf[:dle]
|
||||||
|
return frames
|
||||||
|
if buf[etb] != ETB:
|
||||||
|
pos = dle + 1 # framing mismatch; this DLE was not a real start
|
||||||
|
continue
|
||||||
|
frames.append(bytes(buf[dle + 1:etb])) # 7 header bytes + data
|
||||||
|
pos = etb + 1
|
||||||
|
while pos < len(buf) and buf[pos] in (0x0D, 0x0A):
|
||||||
|
pos += 1 # swallow the trailing \r\n
|
||||||
|
del buf[:pos]
|
||||||
|
pos = 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Serial helpers (lazy pyserial import)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def detect_port():
|
||||||
|
"""Best-guess serial device, or None. Prefers known USB-UART bridges.
|
||||||
|
|
||||||
|
Uses pyserial's enumeration when available, but falls back to a stdlib
|
||||||
|
glob so the extcap config dialog still offers a sensible default before
|
||||||
|
pyserial has been pulled in.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from serial.tools import list_ports
|
||||||
|
except ImportError:
|
||||||
|
import glob
|
||||||
|
cands = sorted(glob.glob("/dev/ttyUSB*")) + sorted(glob.glob("/dev/ttyACM*"))
|
||||||
|
return cands[0] if cands else None
|
||||||
|
|
||||||
|
ports = list(list_ports.comports())
|
||||||
|
if not ports:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def score(p):
|
||||||
|
s = 0
|
||||||
|
if p.vid in PREFERRED_VIDS:
|
||||||
|
s += 10
|
||||||
|
dev = p.device or ""
|
||||||
|
if "ttyUSB" in dev:
|
||||||
|
s += 3
|
||||||
|
elif "ttyACM" in dev:
|
||||||
|
s += 2
|
||||||
|
return s
|
||||||
|
|
||||||
|
ports.sort(key=score, reverse=True)
|
||||||
|
return ports[0].device
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_pyserial():
|
||||||
|
"""Fail with a clear message if pyserial (capture-only) is unavailable."""
|
||||||
|
try:
|
||||||
|
import serial # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
sys.exit("error: capture requires pyserial, which is not installed.\n"
|
||||||
|
"Install it (e.g. `pip install pyserial`), or run this script via "
|
||||||
|
"`uv run --script` to pull in the inline dependency.")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_port(requested):
|
||||||
|
"""Map a requested port (possibly None/"auto") to a concrete device."""
|
||||||
|
if requested and requested != "auto":
|
||||||
|
return requested
|
||||||
|
port = detect_port()
|
||||||
|
if not port:
|
||||||
|
sys.exit("error: no serial port given and none could be autodetected "
|
||||||
|
"(pass --port /dev/ttyUSBx)")
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
|
def open_serial(port):
|
||||||
|
import serial # lazy: only the serial modes need it
|
||||||
|
|
||||||
|
# Flow control fully off: the binary payload can legitimately contain
|
||||||
|
# 0x11 (XON) / 0x13 (XOFF), which a flow-controlled link would eat.
|
||||||
|
return serial.Serial(
|
||||||
|
port,
|
||||||
|
BAUD,
|
||||||
|
timeout=0.2, # short, so signal flags get checked promptly
|
||||||
|
xonxoff=False,
|
||||||
|
rtscts=False,
|
||||||
|
dsrdtr=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Capture (extcap + pipe)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_stop = False
|
||||||
|
|
||||||
|
|
||||||
|
def _request_stop(signum, frame):
|
||||||
|
global _stop
|
||||||
|
_stop = True
|
||||||
|
|
||||||
|
|
||||||
|
def run_capture(out, port):
|
||||||
|
"""Stream frames from the serial port to `out` until told to stop."""
|
||||||
|
ensure_pyserial() # re-execs under uv if pyserial is missing
|
||||||
|
import serial # for SerialException
|
||||||
|
|
||||||
|
port = resolve_port(port)
|
||||||
|
try:
|
||||||
|
ser = open_serial(port)
|
||||||
|
except serial.SerialException as e:
|
||||||
|
sys.exit("error: could not open %s: %s" % (port, e))
|
||||||
|
|
||||||
|
# Put the firmware into binary-frame mode (REPL 'X').
|
||||||
|
try:
|
||||||
|
ser.write(b"X")
|
||||||
|
except serial.SerialException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
buf = bytearray()
|
||||||
|
try:
|
||||||
|
write_global_header(out)
|
||||||
|
while not _stop:
|
||||||
|
try:
|
||||||
|
chunk = ser.read(ser.in_waiting or 1)
|
||||||
|
except serial.SerialException:
|
||||||
|
break # adapter went away -> stop cleanly
|
||||||
|
if not chunk:
|
||||||
|
continue
|
||||||
|
buf.extend(chunk)
|
||||||
|
for payload in extract_frames(buf):
|
||||||
|
write_record(out, time.time_ns(), payload)
|
||||||
|
except (BrokenPipeError, OSError):
|
||||||
|
pass # Wireshark closed the fifo/pipe -> we're done
|
||||||
|
finally:
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Convert (offline text log -> pcap), replacing Julia avclan_text_to_pcap
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
TIME_RE = re.compile(r"^(\d{1,2}):(\d{2}):(\d{2})\.(\d+)$")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_text_line(line, base_date):
|
||||||
|
"""Parse one text-mode log line.
|
||||||
|
|
||||||
|
Returns (ts_ns_or_None, payload) or None if the line is not a frame.
|
||||||
|
Line format (whitespace separated):
|
||||||
|
[HH:MM:SS.mmm] is_unicast controller peripheral control length data...
|
||||||
|
with the numeric fields in C-style notation (0x.. or plain decimal).
|
||||||
|
"""
|
||||||
|
tokens = line.split()
|
||||||
|
if len(tokens) < 5:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ts_ns = None
|
||||||
|
m = TIME_RE.match(tokens[0])
|
||||||
|
if m:
|
||||||
|
h, mn, s, frac = m.groups()
|
||||||
|
micros = int(frac.ljust(6, "0")[:6])
|
||||||
|
try:
|
||||||
|
dt = datetime(base_date.year, base_date.month, base_date.day,
|
||||||
|
int(h), int(mn), int(s))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
ts_ns = int(dt.timestamp()) * 1_000_000_000 + micros * 1000
|
||||||
|
tokens = tokens[1:]
|
||||||
|
|
||||||
|
if len(tokens) < 5:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
is_unicast = int(tokens[0], 0)
|
||||||
|
controller = int(tokens[1], 0)
|
||||||
|
peripheral = int(tokens[2], 0)
|
||||||
|
control = int(tokens[3], 0)
|
||||||
|
length = int(tokens[4], 0)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
data_tokens = tokens[5:]
|
||||||
|
if length > MAX_DATA_LEN or len(data_tokens) != length:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
data = bytes(int(d, 0) & 0xFF for d in data_tokens)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = bytes([
|
||||||
|
is_unicast & 0xFF,
|
||||||
|
(controller >> 8) & 0xFF, controller & 0xFF,
|
||||||
|
(peripheral >> 8) & 0xFF, peripheral & 0xFF,
|
||||||
|
control & 0xFF,
|
||||||
|
length & 0xFF,
|
||||||
|
]) + data
|
||||||
|
return ts_ns, payload
|
||||||
|
|
||||||
|
|
||||||
|
def run_convert(infile, output):
|
||||||
|
# Date comes from the log's mtime (the lines only carry a wall-clock time);
|
||||||
|
# falls back to today if that is unavailable.
|
||||||
|
try:
|
||||||
|
base_date = datetime.fromtimestamp(os.path.getmtime(infile))
|
||||||
|
except OSError:
|
||||||
|
base_date = datetime.now()
|
||||||
|
|
||||||
|
want_pcapng = bool(output) and output.endswith(".pcapng")
|
||||||
|
tmp_path = None
|
||||||
|
if want_pcapng:
|
||||||
|
fd, tmp_path = tempfile.mkstemp(suffix=".pcap")
|
||||||
|
out = os.fdopen(fd, "wb")
|
||||||
|
close_out = True
|
||||||
|
elif output:
|
||||||
|
out = open(output, "wb")
|
||||||
|
close_out = True
|
||||||
|
else:
|
||||||
|
out = sys.stdout.buffer
|
||||||
|
close_out = False
|
||||||
|
|
||||||
|
last_ts = None
|
||||||
|
count = 0
|
||||||
|
try:
|
||||||
|
write_global_header(out)
|
||||||
|
with open(infile, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
parsed = parse_text_line(line, base_date)
|
||||||
|
if parsed is None:
|
||||||
|
continue
|
||||||
|
ts_ns, payload = parsed
|
||||||
|
if ts_ns is None:
|
||||||
|
# No timestamp on this line; nudge past the previous one so
|
||||||
|
# ordering is preserved (mirrors the old Julia behaviour).
|
||||||
|
base = last_ts if last_ts is not None else time.time_ns()
|
||||||
|
ts_ns = base + 1000
|
||||||
|
write_record(out, ts_ns, payload)
|
||||||
|
last_ts = ts_ns
|
||||||
|
count += 1
|
||||||
|
finally:
|
||||||
|
if close_out:
|
||||||
|
out.close()
|
||||||
|
|
||||||
|
if want_pcapng:
|
||||||
|
editcap = shutil.which("editcap")
|
||||||
|
if editcap:
|
||||||
|
subprocess.run([editcap, "-F", "pcapng", tmp_path, output], check=True)
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
else:
|
||||||
|
fallback = output[: -len(".pcapng")] + ".pcap"
|
||||||
|
shutil.move(tmp_path, fallback)
|
||||||
|
print("warning: editcap not found; wrote pcap to %s instead of %s"
|
||||||
|
% (fallback, output), file=sys.stderr)
|
||||||
|
output = fallback
|
||||||
|
|
||||||
|
if output:
|
||||||
|
print("wrote %d frames to %s" % (count, output), file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# extcap argument phases
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def extcap_interfaces():
|
||||||
|
print("extcap {version=1.0}{help=https://github.com/}"
|
||||||
|
"{display=AVC-LAN Mockingboard}")
|
||||||
|
print("interface {value=avclan}{display=AVC-LAN (serial)}")
|
||||||
|
|
||||||
|
|
||||||
|
def extcap_dlts():
|
||||||
|
print("dlt {number=%d}{name=USER15}{display=AVC-LAN / IEBus}" % LINKTYPE_USER15)
|
||||||
|
|
||||||
|
|
||||||
|
def extcap_config():
|
||||||
|
default = detect_port() or "auto"
|
||||||
|
print('arg {number=0}{call=--port}{display=Serial port}{type=string}'
|
||||||
|
'{tooltip=Serial device path, or "auto" to autodetect}'
|
||||||
|
'{default=%s}' % default)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(add_help=True)
|
||||||
|
# extcap discovery/lifecycle flags (Wireshark-driven)
|
||||||
|
parser.add_argument("--extcap-interfaces", action="store_true")
|
||||||
|
parser.add_argument("--extcap-dlts", action="store_true")
|
||||||
|
parser.add_argument("--extcap-config", action="store_true")
|
||||||
|
parser.add_argument("--extcap-interface")
|
||||||
|
parser.add_argument("--extcap-version")
|
||||||
|
parser.add_argument("--capture", action="store_true")
|
||||||
|
parser.add_argument("--fifo")
|
||||||
|
parser.add_argument("--extcap-control-in")
|
||||||
|
parser.add_argument("--extcap-control-out")
|
||||||
|
# shared / CLI
|
||||||
|
parser.add_argument("--port", help='serial device, or "auto"')
|
||||||
|
parser.add_argument("--convert", metavar="LOGFILE",
|
||||||
|
help="convert a text-mode serial log to pcap")
|
||||||
|
parser.add_argument("-o", "--output",
|
||||||
|
help="output file for --convert (.pcap or .pcapng); "
|
||||||
|
"stdout if omitted")
|
||||||
|
args, _ = parser.parse_known_args()
|
||||||
|
|
||||||
|
if args.extcap_interfaces:
|
||||||
|
extcap_interfaces()
|
||||||
|
return
|
||||||
|
if args.extcap_dlts:
|
||||||
|
extcap_dlts()
|
||||||
|
return
|
||||||
|
if args.extcap_config:
|
||||||
|
extcap_config()
|
||||||
|
return
|
||||||
|
if args.convert:
|
||||||
|
run_convert(args.convert, args.output)
|
||||||
|
return
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, _request_stop)
|
||||||
|
signal.signal(signal.SIGTERM, _request_stop)
|
||||||
|
|
||||||
|
if args.capture:
|
||||||
|
if not args.fifo:
|
||||||
|
sys.exit("error: --capture requires --fifo")
|
||||||
|
with open(args.fifo, "wb") as fifo:
|
||||||
|
run_capture(fifo, args.port)
|
||||||
|
return
|
||||||
|
|
||||||
|
# No mode flags: behave as a plain pipe into `wireshark -k -i -`.
|
||||||
|
run_capture(sys.stdout.buffer, args.port)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except BrokenPipeError:
|
||||||
|
# Reader (Wireshark) went away; exit quietly without a traceback.
|
||||||
|
try:
|
||||||
|
sys.stdout.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
os._exit(0)
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""One-off: convert a bracketed-timestamp AVC-LAN `log` dump into a pcap.
|
||||||
|
|
||||||
|
The capture in `msgdumps/log` is a foreign text format (not the firmware's):
|
||||||
|
|
||||||
|
[ 08:50:12.793 ] 1901F1040025C0EF
|
||||||
|
|
||||||
|
Each hex blob is a tight nibble packing of an AVC-LAN frame, with no spaces and
|
||||||
|
no leading is_unicast byte (unlike the firmware text format that
|
||||||
|
extcap/avclan-pcap.py's --convert handles):
|
||||||
|
|
||||||
|
controller : 3 hex nibbles (12-bit address)
|
||||||
|
peripheral : 3 hex nibbles (12-bit address)
|
||||||
|
length : 2 hex nibbles (1 byte: data byte count)
|
||||||
|
data : length bytes (2*length hex nibbles)
|
||||||
|
|
||||||
|
There is no control field in this format; the dissector wants one, so we
|
||||||
|
synthesize the standard AVC-LAN value (0x0F).
|
||||||
|
|
||||||
|
It emits pcap with linktype 162 (DLT_USER15), the payload layout the
|
||||||
|
avclan_plugin.lua dissector expects -- byte-for-byte compatible with
|
||||||
|
avclan-pcap.py's output:
|
||||||
|
|
||||||
|
[is_unicast, ctrl_hi, ctrl_lo, periph_hi, periph_lo, control, length, data...]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# pcap on-the-wire format -- mirrors avclan-pcap.py exactly:
|
||||||
|
# nanosecond magic, snaplen 64, linktype 162 (DLT_USER15 / wtap.USER15).
|
||||||
|
PCAP_MAGIC_NS = 0xA1B23C4D
|
||||||
|
SNAPLEN = 64
|
||||||
|
LINKTYPE_USER15 = 162
|
||||||
|
|
||||||
|
PCAP_GLOBAL_HEADER = struct.pack(
|
||||||
|
"<IHHiIII",
|
||||||
|
PCAP_MAGIC_NS, # magic (nanosecond resolution, little-endian)
|
||||||
|
2, # version major
|
||||||
|
4, # version minor
|
||||||
|
0, # thiszone
|
||||||
|
0, # sigfigs
|
||||||
|
SNAPLEN, # snaplen
|
||||||
|
LINKTYPE_USER15,
|
||||||
|
)
|
||||||
|
|
||||||
|
# AVC-LAN broadcast destination addresses -- a frame to anything else is unicast.
|
||||||
|
BROADCAST_ADDRS = {0x1FF, 0xFFF}
|
||||||
|
# This format omits the control field; synthesize the standard AVC-LAN value.
|
||||||
|
STANDARD_CONTROL = 0x0F
|
||||||
|
|
||||||
|
LINE_RE = re.compile(
|
||||||
|
r"^\[\s*(\d+):(\d+):(\d+)\.(\d+)\s*\]\s*([0-9A-Fa-f]+)\s*$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def write_global_header(out):
|
||||||
|
out.write(PCAP_GLOBAL_HEADER)
|
||||||
|
|
||||||
|
|
||||||
|
def write_record(out, ts_ns, payload):
|
||||||
|
sec, nsec = divmod(ts_ns, 1_000_000_000)
|
||||||
|
out.write(struct.pack("<IIII", sec, nsec, len(payload), len(payload)))
|
||||||
|
out.write(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_line(line, base_date):
|
||||||
|
"""Parse one log line -> (ts_ns, payload) or None if not a frame."""
|
||||||
|
m = LINE_RE.match(line)
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
h, mn, s, frac, hx = m.groups()
|
||||||
|
|
||||||
|
# Header is controller(3) + peripheral(3) + length(2) = 8 nibbles, no control.
|
||||||
|
if len(hx) < 8:
|
||||||
|
return None
|
||||||
|
length = int(hx[6:8], 16)
|
||||||
|
if len(hx) != 8 + 2 * length:
|
||||||
|
return None
|
||||||
|
|
||||||
|
controller = int(hx[0:3], 16)
|
||||||
|
peripheral = int(hx[3:6], 16)
|
||||||
|
control = STANDARD_CONTROL # not present in the log; synthesize the standard
|
||||||
|
is_unicast = 0 if peripheral in BROADCAST_ADDRS else 1
|
||||||
|
data = bytes.fromhex(hx[8:])
|
||||||
|
|
||||||
|
payload = bytes([
|
||||||
|
is_unicast,
|
||||||
|
(controller >> 8) & 0xFF, controller & 0xFF,
|
||||||
|
(peripheral >> 8) & 0xFF, peripheral & 0xFF,
|
||||||
|
control & 0xFF,
|
||||||
|
length & 0xFF,
|
||||||
|
]) + data
|
||||||
|
|
||||||
|
micros = int(frac.ljust(6, "0")[:6])
|
||||||
|
dt = datetime(base_date.year, base_date.month, base_date.day,
|
||||||
|
int(h), int(mn), int(s))
|
||||||
|
return dt, micros, payload
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
parser.add_argument("infile", nargs="?",
|
||||||
|
default=os.path.join(here, "msgdumps", "log"),
|
||||||
|
help="input log file (default: msgdumps/log)")
|
||||||
|
parser.add_argument("-o", "--output",
|
||||||
|
help="output pcap (default: <infile>.pcap)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
output = args.output or (args.infile + ".pcap")
|
||||||
|
|
||||||
|
# Lines carry only wall-clock time; take the date from the log's mtime.
|
||||||
|
try:
|
||||||
|
base_date = datetime.fromtimestamp(os.path.getmtime(args.infile))
|
||||||
|
except OSError:
|
||||||
|
base_date = datetime.now()
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
skipped = 0
|
||||||
|
prev_tod = None # previous (h,m,s,micros) for midnight-rollover detection
|
||||||
|
day_offset = timedelta(0)
|
||||||
|
|
||||||
|
with open(args.infile, "r") as f, open(output, "wb") as out:
|
||||||
|
write_global_header(out)
|
||||||
|
for lineno, line in enumerate(f, 1):
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
parsed = parse_line(line, base_date)
|
||||||
|
if parsed is None:
|
||||||
|
skipped += 1
|
||||||
|
print("skip: malformed line %d: %r" % (lineno, line.rstrip()),
|
||||||
|
file=sys.stderr)
|
||||||
|
continue
|
||||||
|
dt, micros, payload = parsed
|
||||||
|
tod = (dt.hour, dt.minute, dt.second, micros)
|
||||||
|
if prev_tod is not None and tod < prev_tod:
|
||||||
|
day_offset += timedelta(days=1) # wall clock wrapped past midnight
|
||||||
|
prev_tod = tod
|
||||||
|
dt = dt + day_offset
|
||||||
|
ts_ns = int(dt.timestamp()) * 1_000_000_000 + micros * 1000
|
||||||
|
write_record(out, ts_ns, payload)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
print("wrote %d frames to %s%s" % (
|
||||||
|
count, output,
|
||||||
|
" (%d skipped)" % skipped if skipped else ""), file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
module AVCLANPipe
|
|
||||||
|
|
||||||
using PcapTools, Dates, UnixTimes
|
|
||||||
|
|
||||||
export AVCLANframe, avclan_text_to_pcap, tobytes
|
|
||||||
|
|
||||||
mutable struct AVCLANframe
|
|
||||||
broadcast::Bool
|
|
||||||
controller_addr::UInt16
|
|
||||||
peripheral_addr::UInt16
|
|
||||||
control::UInt8
|
|
||||||
length::UInt8
|
|
||||||
data::NTuple{32,UInt8}
|
|
||||||
end
|
|
||||||
|
|
||||||
AVCLANframe() = AVCLANframe(false, 0x0000, 0x0000, 0xf, 0x0, ntuple(x -> 0x0, 32))
|
|
||||||
|
|
||||||
function Base.tryparse(::Type{AVCLANframe}, str::String)
|
|
||||||
vals = split(str)
|
|
||||||
|
|
||||||
length(vals) < 6 && return nothing
|
|
||||||
|
|
||||||
local t
|
|
||||||
try
|
|
||||||
t = Time(first(vals), "HH:MM:SS.s")
|
|
||||||
catch e
|
|
||||||
end
|
|
||||||
if !(@isdefined(t))
|
|
||||||
t = nothing
|
|
||||||
else
|
|
||||||
popfirst!(vals)
|
|
||||||
end
|
|
||||||
|
|
||||||
broadcast = tryparse(Bool, vals[1])
|
|
||||||
isnothing(broadcast) && return nothing
|
|
||||||
|
|
||||||
controller_addr = tryparse(UInt16, vals[2])
|
|
||||||
isnothing(controller_addr) && return nothing
|
|
||||||
|
|
||||||
peripheral_addr = tryparse(UInt16, vals[3])
|
|
||||||
isnothing(peripheral_addr) && return nothing
|
|
||||||
|
|
||||||
control = tryparse(UInt8, vals[4])
|
|
||||||
isnothing(control) && return nothing
|
|
||||||
|
|
||||||
len = tryparse(UInt8, vals[5])
|
|
||||||
isnothing(len) && return nothing
|
|
||||||
|
|
||||||
if (length(vals) - 5) != len || len > 32
|
|
||||||
return nothing
|
|
||||||
end
|
|
||||||
_data = tryparse.(UInt8, vals[6:end])
|
|
||||||
data = ntuple(i -> checkindex(Bool, axes(_data, 1), i) ? _data[i] : 0x0, 32)
|
|
||||||
|
|
||||||
return t, AVCLANframe(broadcast, controller_addr, peripheral_addr, control, len, data)
|
|
||||||
end
|
|
||||||
|
|
||||||
function tobytes(frame::AVCLANframe)
|
|
||||||
data = Vector{UInt8}(undef, 0)
|
|
||||||
push!(data, frame.broadcast)
|
|
||||||
append!(data, reverse(reinterpret(reshape, UInt8, [frame.controller_addr])),
|
|
||||||
reverse(reinterpret(reshape, UInt8, [frame.peripheral_addr])),
|
|
||||||
reinterpret(reshape, UInt8, [frame.control]),
|
|
||||||
reinterpret(reshape, UInt8, [frame.length]),
|
|
||||||
frame.data[1:frame.length])
|
|
||||||
|
|
||||||
return data
|
|
||||||
end
|
|
||||||
|
|
||||||
function avclan_text_to_pcap(textlog::String, pcap_fn::String)
|
|
||||||
pcapstream = PcapStreamWriter(pcap_fn; snaplen=64, linktype=162)
|
|
||||||
t = UnixTime(now())
|
|
||||||
|
|
||||||
for line in eachline(textlog)
|
|
||||||
tstamp_frame = tryparse(AVCLANframe, line)
|
|
||||||
if !isnothing(tstamp_frame)
|
|
||||||
tstamp, frame = tstamp_frame
|
|
||||||
else
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
if !isnothing(tstamp)
|
|
||||||
t = UnixTime(DateTime(today(), tstamp))
|
|
||||||
else
|
|
||||||
t += Microsecond(rand(1:15))
|
|
||||||
end
|
|
||||||
if !isnothing(frame)
|
|
||||||
write(pcapstream, t, tobytes(frame))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
close(pcapstream)
|
|
||||||
|
|
||||||
return pcap_fn
|
|
||||||
end
|
|
||||||
|
|
||||||
end # module AVCLANPipe
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
using AVCLANPipe, PcapTools, Dates, UnixTimes
|
|
||||||
|
|
||||||
file = ARGS[1]
|
|
||||||
outfile = ARGS[2]
|
|
||||||
|
|
||||||
function convert_fields_to_pcap(file, outfile)
|
|
||||||
|
|
||||||
io = open(file, "r")
|
|
||||||
outio = PcapStreamWriter(outfile; snaplen=64, linktype = 162)
|
|
||||||
|
|
||||||
packet = AVCLANframe()
|
|
||||||
packetcomplete = false
|
|
||||||
local t::UnixTime
|
|
||||||
i = 1
|
|
||||||
|
|
||||||
for line in readlines(io)
|
|
||||||
m = match(r"(\d+)-(\d+) IEBus: Raw Fields: (?<field>\w+)(:? ((?<hex>0x[0-9a-f]{2,3})|WRITE_DATA|Length: (?<length>\d+)))?",
|
|
||||||
line)
|
|
||||||
|
|
||||||
if m[:field] == "Broadcast"
|
|
||||||
packet.broadcast = true
|
|
||||||
t = UnixTime(1970,1,1,0,0,0,0, something(tryparse(Int, m[1]), 0))
|
|
||||||
elseif m[:field] == "Unicast"
|
|
||||||
packet.broadcast = false
|
|
||||||
t = UnixTime(1970,1,1,0,0,0,0, something(tryparse(Int, m[1]), 0))
|
|
||||||
elseif m[:field] == "Master"
|
|
||||||
packet.controller_addr = tryparse(UInt16, m[:hex])
|
|
||||||
elseif m[:field] == "Slave"
|
|
||||||
packet.peripheral_addr = tryparse(UInt16, m[:hex])
|
|
||||||
elseif m[:field] == "Data"
|
|
||||||
if !isnothing(m[:length])
|
|
||||||
packet.length = convert(UInt8, tryparse(Int, m[:length]))
|
|
||||||
else
|
|
||||||
data = collect(packet.data)
|
|
||||||
data[i] = tryparse(UInt8, m[:hex])
|
|
||||||
packet.data = ntuple(n -> data[n], 32)
|
|
||||||
i += 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if i > packet.length
|
|
||||||
packetcomplete = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if packetcomplete
|
|
||||||
packetcomplete = false
|
|
||||||
i = 1
|
|
||||||
write(outio, t, tobytes(packet))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
close(io)
|
|
||||||
close(outio)
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
include("AVCLANPipe.jl")
|
|
||||||
|
|
||||||
using .AVCLANPipe, PcapTools, Dates, UnixTimes, LibSerialPort
|
|
||||||
|
|
||||||
serial_port="/dev/ttyUSB1"
|
|
||||||
baud=1200000
|
|
||||||
|
|
||||||
pcapstream = PcapStreamWriter(stdout; snaplen=64, linktype=162)
|
|
||||||
serial = LibSerialPort.open(serial_port, baud)
|
|
||||||
set_flow_control(serial) # Disable flow-control (stops it from eating raw byte 0x11)
|
|
||||||
|
|
||||||
write(serial, 'X')
|
|
||||||
buf = IOBuffer()
|
|
||||||
|
|
||||||
function quit(serialio=serial)
|
|
||||||
close(serialio)
|
|
||||||
exit()
|
|
||||||
end
|
|
||||||
|
|
||||||
while true
|
|
||||||
iswritable(stdout) || isopen(stdout) || quit()
|
|
||||||
if bytesavailable(serial) > 0
|
|
||||||
t = UnixTime(now())
|
|
||||||
line = readline(serial)
|
|
||||||
# allthebytes = Vector{UInt8}(undef, bytesavailable(serial))
|
|
||||||
# readbytes!(serial, allthebytes)
|
|
||||||
# write(buf, allthebytes)
|
|
||||||
# l = findfirst(==('\n'), buf.data)
|
|
||||||
# if !isnothing(l)
|
|
||||||
# println(stderr, "fucku")
|
|
||||||
# bytes = Vector{UInt8}(undef, l)
|
|
||||||
# seekstart(buf)
|
|
||||||
# readbytes!(buf, bytes)
|
|
||||||
# seekend(buf)
|
|
||||||
bytes = Vector{UInt8}(line)
|
|
||||||
if bytes[1] == 0x10 && bytes[end] == 0x17
|
|
||||||
write(pcapstream, t, bytes[2:end-1])
|
|
||||||
else
|
|
||||||
@error line
|
|
||||||
end
|
|
||||||
# end
|
|
||||||
# println(stderr, "wtf")
|
|
||||||
end
|
|
||||||
yield()
|
|
||||||
end
|
|
||||||
Reference in New Issue
Block a user