Skip to content

Commit 7cef027

Browse files
committed
Merged default into feature-core-docs
1 parent 2db1d3a commit 7cef027

File tree

10 files changed

+262
-75
lines changed

10 files changed

+262
-75
lines changed

bin/can_logger.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
parser = argparse.ArgumentParser(description="Log CAN traffic, printing messages to stdout or to a given file")
2828

2929
parser.add_argument("-f", "--file_name", dest="log_file",
30-
help="""Path and base log filename, extension can be .txt, .csv, .db, .npz""",
30+
help="""Path and base log filename, extension can be .txt, .asc, .csv, .db, .npz""",
3131
default=None)
3232

3333
parser.add_argument("-v", action="count", dest="verbosity",
@@ -70,10 +70,12 @@
7070

7171
bus = can.interface.Bus(results.channel, bustype=results.interface, can_filters=can_filters)
7272
print('Can Logger (Started on {})\n'.format(datetime.datetime.now()))
73-
notifier = can.Notifier(bus, [can.Printer(results.log_file)], timeout=0.1)
73+
logger = can.Logger(results.log_file)
74+
notifier = can.Notifier(bus, [logger], timeout=0.1)
7475

7576
try:
7677
while True:
7778
time.sleep(1)
7879
except KeyboardInterrupt:
7980
bus.shutdown()
81+
notifier.stop()

can/CAN.py

Lines changed: 149 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
from __future__ import print_function
99

1010
import logging
11+
from datetime import datetime
12+
import time
13+
import base64
14+
import sqlite3
1115

1216
try:
1317
import queue
@@ -42,6 +46,11 @@ def on_message_received(self, msg):
4246
def __call__(self, msg):
4347
return self.on_message_received(msg)
4448

49+
def stop(self):
50+
"""
51+
Override to cleanup any open resources.
52+
"""
53+
4554

4655
class BufferedReader(Listener):
4756
"""
@@ -60,15 +69,43 @@ def on_message_received(self, msg):
6069
def get_message(self, timeout=0.5):
6170
"""
6271
Attempts to retrieve the latest message received by the instance. If no message is
63-
available it blocks for 0.5 seconds or until a message is received (whichever
64-
is shorter), and returns the message if there is one, or None if there is not.
72+
available it blocks for given timeout or until a message is received (whichever
73+
is shorter),
74+
75+
:param float timeout: The number of seconds to wait for a new message.
76+
:return: the :class:`~can.Message` if there is one, or None if there is not.
6577
"""
6678
try:
6779
return self.buffer.get(block=True, timeout=timeout)
6880
except queue.Empty:
6981
return None
7082

7183

84+
class Logger(object):
85+
"""
86+
Logs CAN messages to a file.
87+
88+
The format is determined from the file format which can be one of:
89+
* .asc: :class:`can.ASCWriter`
90+
* .csv: :class:`can.CSVWriter`
91+
* .db: :class:`can.SqliteWriter`
92+
* other: :class:`can.Printer`
93+
"""
94+
95+
@classmethod
96+
def __new__(cls, other, filename):
97+
if not filename:
98+
return Printer()
99+
elif filename.endswith(".asc"):
100+
return ASCWriter(filename)
101+
elif filename.endswith(".csv"):
102+
return CSVWriter(filename)
103+
elif filename.endswith(".db"):
104+
return SqliteWriter(filename)
105+
else:
106+
return Printer(filename)
107+
108+
72109
class Printer(Listener):
73110
"""
74111
The Printer class is a subclass of :class:`~can.Listener` which simply prints
@@ -89,9 +126,9 @@ def on_message_received(self, msg):
89126
else:
90127
print(msg)
91128

92-
def __del__(self):
93-
self.output_file.write("\n")
129+
def stop(self):
94130
if self.output_file:
131+
self.output_file.write("\n")
95132
self.output_file.close()
96133

97134

@@ -105,30 +142,125 @@ def __init__(self, filename):
105142
self.csv_file = open(filename, 'wt')
106143

107144
# Write a header row
108-
self.csv_file.write("timestamp, arbitrationid, flags, dlc, data")
145+
self.csv_file.write("timestamp, arbitration id, extended, remote, error, dlc, data\n")
109146

110147
def on_message_received(self, msg):
111-
row = ','.join([msg.timestamp,
112-
msg.arbitration_id,
113-
msg.flags,
114-
msg.dlc,
115-
msg.data])
148+
row = ','.join([
149+
str(msg.timestamp),
150+
hex(msg.arbitration_id),
151+
'1' if msg.id_type else '0',
152+
'1' if msg.is_remote_frame else '0',
153+
'1' if msg.is_error_frame else '0',
154+
str(msg.dlc),
155+
base64.b64encode(msg.data).decode('utf8')
156+
])
116157
self.csv_file.write(row + '\n')
117158

118-
def __del__(self):
159+
def stop(self):
160+
self.csv_file.flush()
119161
self.csv_file.close()
120-
super(CSVWriter, self).__del__()
121162

122163

123164
class SqliteWriter(Listener):
124-
"""TODO"""
165+
"""Logs received CAN data to a simple SQL database.
166+
167+
The sqlite database may already exist, otherwise it will
168+
be created when the first message arrives.
169+
"""
170+
171+
insert_msg_template = '''
172+
INSERT INTO messages VALUES
173+
(?, ?, ?, ?, ?, ?, ?)
174+
'''
125175

126176
def __init__(self, filename):
127-
self.db_file = open(filename, 'wt')
177+
self.db_fn = filename
178+
self.db_setup = False
179+
180+
def _create_db(self):
181+
# Note you can't share sqlite3 connections between threads
182+
# hence we setup the db here.
183+
log.info("Creating sqlite db")
184+
self.conn = sqlite3.connect(self.db_fn)
185+
c = self.conn.cursor()
128186

129187
# create table structure
130-
raise NotImplementedError("TODO")
188+
c.execute('''
189+
CREATE TABLE IF NOT EXISTS messages
190+
(
191+
ts REAL,
192+
arbitration_id INTEGER,
193+
extended INTEGER,
194+
remote INTEGER,
195+
error INTEGER,
196+
dlc INTEGER,
197+
data BLOB
198+
)
199+
''')
200+
self.conn.commit()
201+
202+
self.db_setup = True
203+
204+
def on_message_received(self, msg):
205+
if not self.db_setup:
206+
self._create_db()
207+
208+
# add row to db
209+
row_data = (
210+
msg.timestamp,
211+
msg.arbitration_id,
212+
msg.id_type,
213+
msg.is_remote_frame,
214+
msg.is_error_frame,
215+
msg.dlc,
216+
msg.data
217+
)
218+
c = self.conn.cursor()
219+
c.execute(SqliteWriter.insert_msg_template, row_data)
220+
221+
self.conn.commit()
222+
223+
def stop(self):
224+
if self.db_setup:
225+
self.conn.commit()
226+
self.conn.close()
227+
228+
229+
class ASCWriter(Listener):
230+
"""Logs CAN data to an ASCII log file (.asc)"""
231+
232+
LOG_STRING = "{time: 9.4f} {channel} {id:<15} Rx d {dlc} {data}\n"
233+
234+
def __init__(self, filename):
235+
now = datetime.now().strftime("%a %b %m %I:%M:%S %p %Y")
236+
self.start = time.time()
237+
self.log_file = open(filename, "w")
238+
self.log_file.write("date %s\n" % now)
239+
self.log_file.write("base hex timestamps absolute\n")
240+
self.log_file.write("internal events logged\n")
241+
self.log_file.write("Begin Triggerblock %s\n" % now)
242+
self.log_file.write(" 0.0000 Start of measurement\n")
243+
244+
def stop(self):
245+
"""Stops logging and closes the file."""
246+
if self.log_file:
247+
self.log_file.write("End TriggerBlock\n")
248+
self.log_file.close()
249+
self.log_file = None
131250

132251
def on_message_received(self, msg):
133-
# add row
134-
raise NotImplementedError("TODO")
252+
data = ["{:02X}".format(byte) for byte in msg.data]
253+
arb_id = "{:X}".format(msg.arbitration_id)
254+
if msg.id_type:
255+
arb_id = arb_id + "x"
256+
timestamp = msg.timestamp
257+
if timestamp >= self.start:
258+
timestamp -= self.start
259+
260+
line = self.LOG_STRING.format(time=timestamp,
261+
channel=1,
262+
id=arb_id,
263+
dlc=msg.dlc,
264+
data=" ".join(data))
265+
if self.log_file:
266+
self.log_file.write(line)

can/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
class CanError(IOError):
1717
pass
1818

19-
from can.CAN import BufferedReader, Listener, Printer, CSVWriter, SqliteWriter, set_logging_level
19+
from can.CAN import BufferedReader, Listener, Logger, Printer, CSVWriter, SqliteWriter, ASCWriter, set_logging_level
2020
from can.message import Message
2121
from can.bus import BusABC
2222
from can.notifier import Notifier

can/interfaces/interface.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class Bus(object):
1010
"""
1111
Instantiates a CAN Bus of the given `bustype`, falls back to reading a
1212
configuration file from default locations.
13+
14+
:raises: NotImplementedError if the bustype isn't recognized
15+
1316
"""
1417

1518
@classmethod

can/interfaces/ixxat/canlib.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,13 +240,13 @@ def __init__(self, channel, can_filters=None, **config):
240240
self.CHANNEL_BITRATES[1][bitrate]
241241
)
242242
_canlib.canControlGetCaps(self._control_handle, ctypes.byref(self._channel_capabilities))
243+
243244
# With receive messages, this field contains the relative reception time of
244245
# the message in ticks. The resolution of a tick can be calculated from the fields
245-
# dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accor-
246-
# dance with the following formula:
247-
# Resolution [s] = dwTscDivisor / dwClockFreq
248-
# Reversed the terms, so that we have the number of ticks in a second
249-
self._tick_resolution = self._channel_capabilities.dwClockFreq / self._channel_capabilities.dwTscDivisor
246+
# dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula:
247+
# frequency [1/s] = dwClockFreq / dwTscDivisor
248+
# We explicitly cast to float for Python 2.x users
249+
self._tick_resolution = float(self._channel_capabilities.dwClockFreq / self._channel_capabilities.dwTscDivisor)
250250

251251
# Setup filters before starting the channel
252252
if can_filters is not None and len(can_filters):

0 commit comments

Comments
 (0)