Compare commits
14 Commits
4be733d6e3
...
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 663d75174e | |||
| 324aa5652c | |||
| e27351aa28 | |||
| 6e386fcb4b | |||
| ca670c9bb7 | |||
| 725c9ccbc1 | |||
| 9d5cd74ef7 | |||
| 6ef0571e4e | |||
| c758aca6c6 | |||
| 21172e020b | |||
| ce20587591 | |||
| 2fcaef5571 | |||
| ecef35918e | |||
| 86b14af3c7 |
@@ -0,0 +1 @@
|
|||||||
|
3.14
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
GNU GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU General Public License is a free, copyleft license for
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
software and other kinds of works.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
the GNU General Public License is intended to guarantee your freedom to
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
share and change all versions of a program--to make sure it remains free
|
share and change all versions of a program--to make sure it remains free
|
||||||
software for all its users. We, the Free Software Foundation, use the
|
software for all its users.
|
||||||
GNU General Public License for most of our software; it applies also to
|
|
||||||
any other work released this way by its authors. You can apply it to
|
|
||||||
your programs, too.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
|||||||
want it, that you can change the software or use pieces of it in new
|
want it, that you can change the software or use pieces of it in new
|
||||||
free programs, and that you know you can do these things.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
To protect your rights, we need to prevent others from denying you
|
Developers that use our General Public Licenses protect your rights
|
||||||
these rights or asking you to surrender the rights. Therefore, you have
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
certain responsibilities if you distribute copies of the software, or if
|
you this License which gives you legal permission to copy, distribute
|
||||||
you modify it: responsibilities to respect the freedom of others.
|
and/or modify the software.
|
||||||
|
|
||||||
For example, if you distribute copies of such a program, whether
|
A secondary benefit of defending all users' freedom is that
|
||||||
gratis or for a fee, you must pass on to the recipients the same
|
improvements made in alternate versions of the program, if they
|
||||||
freedoms that you received. You must make sure that they, too, receive
|
receive widespread use, become available for other developers to
|
||||||
or can get the source code. And you must show them these terms so they
|
incorporate. Many developers of free software are heartened and
|
||||||
know their rights.
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
Developers that use the GNU GPL protect your rights with two steps:
|
The GNU Affero General Public License is designed specifically to
|
||||||
(1) assert copyright on the software, and (2) offer you this License
|
ensure that, in such cases, the modified source code becomes available
|
||||||
giving you legal permission to copy, distribute and/or modify it.
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
For the developers' and authors' protection, the GPL clearly explains
|
An older license, called the Affero General Public License and
|
||||||
that there is no warranty for this free software. For both users' and
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
authors' sake, the GPL requires that modified versions be marked as
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
changed, so that their problems will not be attributed erroneously to
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
authors of previous versions.
|
this license.
|
||||||
|
|
||||||
Some devices are designed to deny users access to install or run
|
|
||||||
modified versions of the software inside them, although the manufacturer
|
|
||||||
can do so. This is fundamentally incompatible with the aim of
|
|
||||||
protecting users' freedom to change the software. The systematic
|
|
||||||
pattern of such abuse occurs in the area of products for individuals to
|
|
||||||
use, which is precisely where it is most unacceptable. Therefore, we
|
|
||||||
have designed this version of the GPL to prohibit the practice for those
|
|
||||||
products. If such problems arise substantially in other domains, we
|
|
||||||
stand ready to extend this provision to those domains in future versions
|
|
||||||
of the GPL, as needed to protect the freedom of users.
|
|
||||||
|
|
||||||
Finally, every program is threatened constantly by software patents.
|
|
||||||
States should not allow patents to restrict development and use of
|
|
||||||
software on general-purpose computers, but in those that do, we wish to
|
|
||||||
avoid the special danger that patents applied to a free program could
|
|
||||||
make it effectively proprietary. To prevent this, the GPL assures that
|
|
||||||
patents cannot be used to render the program non-free.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -72,7 +60,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
0. Definitions.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU General Public License.
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
works, such as semiconductor masks.
|
||||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
|||||||
the Program, the only way you could satisfy both those terms and this
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Use with the GNU Affero General Public License.
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
permission to link or combine any covered work with a work licensed
|
||||||
under version 3 of the GNU Affero General Public License into a single
|
under version 3 of the GNU General Public License into a single
|
||||||
combined work, and to convey the resulting work. The terms of this
|
combined work, and to convey the resulting work. The terms of this
|
||||||
License will continue to apply to the part which is the covered work,
|
License will continue to apply to the part which is the covered work,
|
||||||
but the special requirements of the GNU Affero General Public License,
|
but the work with which it is combined will remain governed by version
|
||||||
section 13, concerning interaction through a network will apply to the
|
3 of the GNU General Public License.
|
||||||
combination as such.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
the GNU General Public License from time to time. Such new versions will
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
be similar in spirit to the present version, but may differ in detail to
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU General
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
Public License "or any later version" applies to it, you have the
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
Foundation. If the Program does not specify a version number of the
|
||||||
GNU General Public License, you may choose any version ever published
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
by the Free Software Foundation.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Program specifies that a proxy can decide which future
|
||||||
versions of the GNU General Public License can be used, that proxy's
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
public statement of acceptance of a version permanently authorizes you
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
to choose that version for the Program.
|
||||||
|
|
||||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
|||||||
Copyright (C) <year> <name of author>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU Affero General Public License as published
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
GNU General Public License for more details.
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU Affero General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If the program does terminal interaction, make it output a short
|
If your software can interact with users remotely through a computer
|
||||||
notice like this when it starts in an interactive mode:
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
<program> Copyright (C) <year> <name of author>
|
interface could display a "Source" link that leads users to an archive
|
||||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
of the code. There are many ways you could offer source, and different
|
||||||
This is free software, and you are welcome to redistribute it
|
solutions will be better for different programs; see section 13 for the
|
||||||
under certain conditions; type `show c' for details.
|
specific requirements.
|
||||||
|
|
||||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
|
||||||
parts of the General Public License. Of course, your program's commands
|
|
||||||
might be different; for a GUI interface, you would use an "about box".
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU GPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<https://www.gnu.org/licenses/>.
|
<http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
The GNU General Public License does not permit incorporating your program
|
|
||||||
into proprietary programs. If your program is a subroutine library, you
|
|
||||||
may consider it more useful to permit linking proprietary applications with
|
|
||||||
the library. If this is what you want to do, use the GNU Lesser General
|
|
||||||
Public License instead of this License. But first, please read
|
|
||||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import re
|
|
||||||
import csv
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def parse_can_log(input_path: str | Path, out_bus1: str | Path, out_bus2: str | Path) -> None:
|
|
||||||
"""
|
|
||||||
Parses a CAN bus log file from CANdigger and saves valid frames to separate CSV files for Bus 1 and Bus 2.
|
|
||||||
Corrupted, incomplete, or debug frames are silently discarded.
|
|
||||||
"""
|
|
||||||
input_file = Path(input_path)
|
|
||||||
out1_file = Path(out_bus1)
|
|
||||||
out2_file = Path(out_bus2)
|
|
||||||
|
|
||||||
# Group 1: Bus (C1 or C2)
|
|
||||||
# Group 2: ID (1 to 8 hex chars)
|
|
||||||
# Group 3: DLC (1 to 2 hex chars)
|
|
||||||
start_pattern = re.compile(r'(C[12]):([0-9A-Fa-f]{1,8})\s+([0-9A-Fa-f]{1,2})\s+')
|
|
||||||
|
|
||||||
byte_pattern = re.compile(r'^[0-9A-Fa-f]{2}$')
|
|
||||||
|
|
||||||
with input_file.open('r', encoding='utf-8') as f_in, \
|
|
||||||
out1_file.open('w', newline='', encoding='utf-8') as f_out1, \
|
|
||||||
out2_file.open('w', newline='', encoding='utf-8') as f_out2:
|
|
||||||
|
|
||||||
writer1 = csv.writer(f_out1)
|
|
||||||
writer2 = csv.writer(f_out2)
|
|
||||||
|
|
||||||
writer1.writerow(['ID', 'DLC', 'Data'])
|
|
||||||
writer2.writerow(['ID', 'DLC', 'Data'])
|
|
||||||
|
|
||||||
for line in f_in:
|
|
||||||
for match in start_pattern.finditer(line):
|
|
||||||
bus = match.group(1)
|
|
||||||
can_id = match.group(2).upper()
|
|
||||||
dlc_str = match.group(3)
|
|
||||||
|
|
||||||
try:
|
|
||||||
dlc = int(dlc_str, 16)
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if dlc > 8:
|
|
||||||
continue
|
|
||||||
|
|
||||||
remaining = line[match.end():]
|
|
||||||
tokens = remaining.split()
|
|
||||||
|
|
||||||
data_bytes = []
|
|
||||||
for token in tokens:
|
|
||||||
if byte_pattern.match(token):
|
|
||||||
data_bytes.append(token.upper())
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
if len(data_bytes) == dlc:
|
|
||||||
data_str = ' '.join(data_bytes)
|
|
||||||
row = [can_id, dlc, data_str]
|
|
||||||
|
|
||||||
if bus == 'C1':
|
|
||||||
writer1.writerow(row)
|
|
||||||
elif bus == 'C2':
|
|
||||||
writer2.writerow(row)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Example usage:
|
|
||||||
# parse_can_log('can_traffic.txt', 'bus1_output.csv', 'bus2_output.csv')
|
|
||||||
# or on CLI:
|
|
||||||
# python3 can_parser.py can_traffic.txt' bus1_output.csv bus2_output.csv
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Parse CAN bus logs to separate CSV files")
|
|
||||||
parser.add_argument("input", help="Path to the input .txt log file")
|
|
||||||
parser.add_argument("out_bus1", help="Output CSV filename for Bus 1 (C1)")
|
|
||||||
parser.add_argument("out_bus2", help="Output CSV filename for Bus 2 (C2)")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
parse_can_log(args.input, args.out_bus1, args.out_bus2)
|
|
||||||
|
|
||||||
pass
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[project]
|
||||||
|
name = "CANveyor"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "J1939 CAN bus parser that works in pair with CANdigger"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
dependencies = ["polars", "pathlib", "typing"]
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import argparse
|
||||||
|
import polars as pl
|
||||||
|
|
||||||
|
def get_j1939_mask() -> pl.Expr:
|
||||||
|
"""
|
||||||
|
Returns a Polars expression representing the strict J1939 filtering rules.
|
||||||
|
"""
|
||||||
|
id_int = pl.col("ID").str.to_integer(base=16).cast(pl.UInt32)
|
||||||
|
return (
|
||||||
|
(id_int > 0x7FF) &
|
||||||
|
((id_int % 33554432 // 16777216) == 0) &
|
||||||
|
(pl.col("DLC") <= 8)
|
||||||
|
)
|
||||||
|
|
||||||
|
def decode_j1939_metadata(lf: pl.LazyFrame) -> pl.LazyFrame:
|
||||||
|
"""
|
||||||
|
Decodes J1939 fields and bundles them into a Struct column.
|
||||||
|
"""
|
||||||
|
id_int = pl.col("ID").str.to_integer(base=16).cast(pl.UInt32)
|
||||||
|
|
||||||
|
id_shifted_8 = id_int // 256
|
||||||
|
id_shifted_16 = id_int // 65536
|
||||||
|
|
||||||
|
priority = ((id_int // 67108864) % 8).cast(pl.UInt8)
|
||||||
|
pf = (id_shifted_16 % 256).cast(pl.UInt8)
|
||||||
|
ps = (id_shifted_8 % 256).cast(pl.UInt8)
|
||||||
|
sa = (id_int % 256).cast(pl.UInt8)
|
||||||
|
|
||||||
|
da = pl.when(pf < 240).then(ps).otherwise(pl.lit(255, dtype=pl.UInt8))
|
||||||
|
|
||||||
|
pgn = pl.when(pf < 240).then(id_shifted_8 % 65536).otherwise(id_shifted_8 % 262144).cast(pl.UInt32)
|
||||||
|
|
||||||
|
return lf.with_columns(
|
||||||
|
pl.struct([
|
||||||
|
priority.alias("Priority"),
|
||||||
|
pf.alias("PF"),
|
||||||
|
ps.alias("PS"),
|
||||||
|
sa.alias("SA"),
|
||||||
|
da.alias("DA"),
|
||||||
|
pgn.alias("PGN")
|
||||||
|
]).alias("j1939_metadata")
|
||||||
|
)
|
||||||
|
|
||||||
|
def decode_j1939_frames(df: pl.DataFrame) -> pl.DataFrame:
|
||||||
|
id_int = pl.col("ID").str.to_integer(base=16).cast(pl.UInt32)
|
||||||
|
|
||||||
|
is_j1939 = (id_int > 0x7FF) & ((id_int % 33554432 // 16777216) == 0) & (pl.col("DLC") <= 8)
|
||||||
|
|
||||||
|
priority = (id_int // 67108864) % 8
|
||||||
|
pf = (id_int // 65536) % 256
|
||||||
|
ps = (id_int // 256) % 256
|
||||||
|
sa = id_int % 256
|
||||||
|
da = pl.when(pf < 240).then(ps).otherwise(255)
|
||||||
|
pgn = pl.when(pf < 240).then((id_int // 256) % 65536).otherwise((id_int // 256) % 262144)
|
||||||
|
|
||||||
|
j1939_meta = pl.when(is_j1939).then(
|
||||||
|
pl.struct([
|
||||||
|
priority.cast(pl.UInt8).alias("Priority"),
|
||||||
|
pf.cast(pl.UInt8).alias("PF"),
|
||||||
|
ps.cast(pl.UInt8).alias("PS"),
|
||||||
|
sa.cast(pl.UInt8).alias("SA"),
|
||||||
|
da.cast(pl.UInt8).alias("DA"),
|
||||||
|
pgn.cast(pl.UInt32).alias("PGN")
|
||||||
|
])
|
||||||
|
).otherwise(None)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="J1939 decoder")
|
||||||
|
parser.add_argument("input_parquet", help="Path to the raw .parquet file")
|
||||||
|
parser.add_argument("output_parquet", help="Path to save the decoded .parquet file")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
df = pl.scan_parquet(args.input_parquet).collect()
|
||||||
|
decoded_df = decode_j1939_frames(df)
|
||||||
|
decoded_df.write_parquet(args.output_parquet)
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
import re
|
||||||
|
import csv
|
||||||
|
import polars as pl
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
PathLike = Union[str, Path]
|
||||||
|
|
||||||
|
def parse_log(input_path: PathLike, out_bus1: PathLike, out_bus2: PathLike) -> None:
|
||||||
|
"""
|
||||||
|
Parses a raw CAN bus log from CANdigger using regex and saves valid frames to separate CSV.
|
||||||
|
Corrupted, incomplete, or debug frames are silently discarded.
|
||||||
|
"""
|
||||||
|
input_file = Path(input_path)
|
||||||
|
out1_file = Path(out_bus1)
|
||||||
|
out2_file = Path(out_bus2)
|
||||||
|
|
||||||
|
start_pattern = re.compile(r'(C[12]):([0-9A-Fa-f]{1,8})\s+([0-9A-Fa-f]{1,2})\s+')
|
||||||
|
byte_pattern = re.compile(r'^[0-9A-Fa-f]{2}$')
|
||||||
|
|
||||||
|
with input_file.open('r', encoding='utf-8') as f_in, \
|
||||||
|
out1_file.open('w', newline='', encoding='utf-8') as f_out1, \
|
||||||
|
out2_file.open('w', newline='', encoding='utf-8') as f_out2:
|
||||||
|
|
||||||
|
writer1 = csv.writer(f_out1)
|
||||||
|
writer2 = csv.writer(f_out2)
|
||||||
|
|
||||||
|
writer1.writerow(['ID', 'DLC', 'Data'])
|
||||||
|
writer2.writerow(['ID', 'DLC', 'Data'])
|
||||||
|
|
||||||
|
for line in f_in:
|
||||||
|
for match in start_pattern.finditer(line):
|
||||||
|
bus = match.group(1)
|
||||||
|
can_id = match.group(2).upper()
|
||||||
|
dlc_str = match.group(3)
|
||||||
|
|
||||||
|
try:
|
||||||
|
dlc = int(dlc_str, 16)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if dlc > 8:
|
||||||
|
continue
|
||||||
|
|
||||||
|
remaining = line[match.end():]
|
||||||
|
tokens = remaining.split()
|
||||||
|
|
||||||
|
data_bytes = []
|
||||||
|
for token in tokens:
|
||||||
|
if byte_pattern.match(token):
|
||||||
|
data_bytes.append(token.upper())
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(data_bytes) == dlc:
|
||||||
|
data_str = ' '.join(data_bytes)
|
||||||
|
row = [can_id, dlc, data_str]
|
||||||
|
|
||||||
|
if bus == 'C1':
|
||||||
|
writer1.writerow(row)
|
||||||
|
elif bus == 'C2':
|
||||||
|
writer2.writerow(row)
|
||||||
|
|
||||||
|
def parse_csv(csv_path: PathLike) -> pl.LazyFrame:
|
||||||
|
"""
|
||||||
|
Ingests a parsed CSV file, unpacks hex strings into 8 hex columns,
|
||||||
|
generates sequential timestamps if missing, and returns a Polars LazyFrame.
|
||||||
|
"""
|
||||||
|
lf = pl.scan_csv(csv_path, schema_overrides={"ID": pl.String, "Data": pl.String})
|
||||||
|
|
||||||
|
if "Timestamp" not in lf.columns:
|
||||||
|
lf = lf.with_row_index("Timestamp")
|
||||||
|
|
||||||
|
byte_exprs = []
|
||||||
|
for i in range(8):
|
||||||
|
expr = (
|
||||||
|
pl.col("Data").str.strip_chars().str.split(" ")
|
||||||
|
.list.get(i, null_on_oob=True)
|
||||||
|
.alias(f"b{i}")
|
||||||
|
)
|
||||||
|
byte_exprs.append(expr)
|
||||||
|
|
||||||
|
lf = lf.with_columns(byte_exprs).drop("Data")
|
||||||
|
|
||||||
|
return lf.with_columns([
|
||||||
|
pl.col("DLC").cast(pl.UInt8),
|
||||||
|
pl.col("Timestamp").cast(pl.Float64)
|
||||||
|
])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="CAN Bus Data Engine & Parser")
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True, help="Available commands")
|
||||||
|
|
||||||
|
parser_csv = subparsers.add_parser("csv", help="Parse raw text log into Bus 1 and Bus 2 CSVs")
|
||||||
|
parser_csv.add_argument("input", help="Path to the .txt log file from CANdigger")
|
||||||
|
parser_csv.add_argument("out_bus1", help="Output CSV filename for Bus 1 (C1)")
|
||||||
|
parser_csv.add_argument("out_bus2", help="Output CSV filename for Bus 2 (C2)")
|
||||||
|
|
||||||
|
parser_parquet = subparsers.add_parser("parquet", help="Convert a parsed CSV into an optimized Parquet file")
|
||||||
|
parser_parquet.add_argument("input_csv", help="Path to the input .csv file")
|
||||||
|
parser_parquet.add_argument("output_parquet", help="Path to the output .parquet file")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == "csv":
|
||||||
|
parse_log(args.input, args.out_bus1, args.out_bus2)
|
||||||
|
print(f"[+] Saved csv file to {args.out_bus1} and {args.out_bus2}")
|
||||||
|
|
||||||
|
elif args.command == "parquet":
|
||||||
|
print(f"[*] Processing {args.input_csv}...")
|
||||||
|
lf = parse_csv(args.input_csv)
|
||||||
|
lf.sink_parquet(args.output_parquet)
|
||||||
|
print(f"[+] Saved parquet file to {args.output_parquet}")
|
||||||
Reference in New Issue
Block a user