├── .gitignore ├── LICENSE ├── README.md ├── client ├── client.c └── makefile ├── examples ├── log_binary_main.py ├── log_to_binary.py └── send_loopback.py ├── setup.py └── socketsocketcan ├── __init__.py └── tcpbus └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IPython 78 | profile_default/ 79 | ipython_config.py 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | .dmypy.json 112 | dmypy.json 113 | 114 | # Pyre type checker 115 | .pyre/ 116 | 117 | .vscode 118 | *.log 119 | 120 | .notes.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Thomas Bruen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SocketCAN over TCP 2 | For when you want to use the [Python CAN package](https://github.com/hardbyte/python-can) but are running on a resource-constrained device, such as a Raspberry Pi. 3 | 4 | I've found that for a busy bus (> 1000 messages/ second), my Raspberry Pi was dropping messages: we'd reached the limit of what Python could achieve. However, it's still a really powerful library for processing messages and integrating other services. 5 | 6 | This package lets you run `python-can` on a more powerful machine, by setting up a TCP connection to the Linux device. A small C client runs on the Linux device to send/ receive CAN messages (with timestamp). This is perfect for listening to a bus and sending occasional messages. I haven't tested this\* with anything that demands high-frequency periodic sending of messages, but using the CAN broadcast manager will probably be better than using this directly. 7 | 8 | \* *I haven't tested this much at all. Sorry. I did have 3 terminals running `cangen` at a 1ms interval for 10 minutes, and didn't have any issues (it was identical to `candump` except where `candump` locked up and dropped a few hundred messages).* 9 | 10 | # CAN Set-up 11 | Enable `vcan` or `can0` (or whichever) on the Linux device: 12 | ``` 13 | sudo modprobe vcan 14 | sudo ip link add dev vcan0 type vcan 15 | sudo ip link set vcan0 up 16 | ``` 17 | or 18 | ``` 19 | sudo ip link set can0 up type can bitrate 1000000 20 | ``` 21 | 22 | # Installation 23 | ## Client 24 | Copy the `client` folder to your Linux device and run `make` 25 | 26 | By default the client is set to receive the message you've sent (personally I've found this really useful for logging because the timestamps are aligned), but you can disable it in `client.c`: `#define RECV_OWN_MSGS 0` 27 | 28 | ## Server 29 | Using a virtual environment is really recommended here. 30 | 31 | `pip install -e path/to/socketsocketcan-repo` 32 | 33 | # Usage 34 | On creating a `TCPBus` object, it will block until a connection with the client is made (i.e. start the client now). Then, you can use the bus to `send()` and `recv()` messages as usual. By default, the bus accepts connections from any host, you can limit this using the `hostname="A_PARTICULAR_HOSTNAME"`keyword arguent. 35 | 36 | The `TCPBus` runs a couple of threads in the background: one to write 'sent' messages to the socket, so that the client can read them in and actually put them on the CAN bus, and another to receive and messages from the client and put them in a queue so the `recv` method can retrieve them. 37 | 38 | As the quotes above impied, calling `send()` doesn't actually send the message. It puts the message on a queue, which a thread will write to the socket as soon as possible. (I haven't looked at how long it actually takes from putting it on the queue and it actually being sent: it will vary depending on resources and you'd have to make sure the clocks on the two computers are synchronised to the sub millisecond level.) 39 | 40 | The client has a similar structure: there is one thread to poll the CAN bus for new messages and put them in a buffer, a second thread to copy from that buffer to the TCP socket, and a third to read from the TCP socket and send the messages via the CAN Bus. 41 | 42 | ## Client 43 | `./client CANCHANNEL HOSTNAME PORT` 44 | e.g. `./client vcan0 my-can-server 5000` 45 | 46 | ## Server 47 | ``` 48 | from socketsocketcan import TCPBus 49 | from can import Message 50 | bus = TCPBus(5000) #start the client now. 51 | bus.send(Message(arbitration_id=0x100,data=list(range(5)))) 52 | print(bus.recv()) #this will be the message you just send (unless) 53 | ``` 54 | 55 | -------------------------------------------------------------------------------- /client/client.c: -------------------------------------------------------------------------------- 1 | /* 2 | TODO: error frames aren't being looped back 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #define HOSTNAME_LEN 128 26 | #define BUF_SZ 100000 27 | 28 | /* CONFIG PARAMETERS */ 29 | #define DEBUG 0 30 | #define RECV_OWN_MSGS 1// if 1, we well receive messages we sent. useful for logging. 31 | const int LOOPBACK = RECV_OWN_MSGS; 32 | 33 | /* DEFINITIONS */ 34 | typedef struct 35 | { 36 | time_t tv_sec; 37 | suseconds_t tv_usec; 38 | canid_t id; 39 | uint8_t dlc;//be careful, when serializing, struct seems to pad this to 4 bytes 40 | uint8_t data[CAN_MAX_DLEN]; 41 | } timestamped_frame; 42 | 43 | typedef struct 44 | { 45 | int tcp_sock; 46 | int can_sock; 47 | } can_write_sockets; // used to supply multiple thread args. 48 | 49 | /* FUNCTION DECLARATIONS */ 50 | 51 | // controlled exit in event of SIGINT 52 | void handle_signal(int signal); 53 | 54 | // print error information and exit (use before therads set-up) 55 | void error(char* msg); 56 | 57 | // open up a TCP connection to the server 58 | int create_tcp_socket(char* hostname, int port); 59 | 60 | // open a CAN socket 61 | int open_can_socket(const char *port); 62 | 63 | // read a single CAN frame and add timestamp 64 | // int read_frame(int soc,timestamped_frame* tf); 65 | int read_frame(int soc,struct can_frame* frame,struct timeval* tv); 66 | 67 | // continually read CAN and add to buffer 68 | void* read_poll_can(void *args); 69 | 70 | // continually dump read CAN buffer to TCP 71 | void* read_poll_tcp(void *args); 72 | 73 | // read from TCP and write to CAN socket 74 | void* write_poll(void *args); 75 | 76 | // convert bytes back to timestamped_can 77 | void deserialize_frame(char* ptr,timestamped_frame* tf); 78 | 79 | // print CAN frame into to stdout (for debug) 80 | void print_frame(timestamped_frame* tf); 81 | 82 | /* GLOBALS */ 83 | pthread_mutex_t read_mutex = PTHREAD_MUTEX_INITIALIZER; 84 | sig_atomic_t poll = true; // for "infinite loops" in threads. 85 | bool tcp_ready_to_send = true; // only access inside of mutex 86 | size_t socketcan_bytes_available;// only access inside of mutex 87 | 88 | pthread_cond_t tcp_send_copied; //signal to enable thread. 89 | 90 | char read_buf_can[BUF_SZ]; // where serialized CAN frames are dumped 91 | char read_buf_tcp[BUF_SZ]; // where serialized CAN frames are copied to and sent to the server 92 | 93 | /* FUNCTIONS */ 94 | void handle_signal(int signal) { 95 | if (signal == SIGINT) 96 | { 97 | poll = false; 98 | } 99 | //TODO: else what? 100 | } 101 | void error(char* msg) 102 | { 103 | perror(msg); 104 | exit(0); 105 | } 106 | 107 | int create_tcp_socket(char* hostname, int port) 108 | { 109 | struct sockaddr_in serv_addr; 110 | struct hostent *server; 111 | int fd; 112 | 113 | fd = socket(AF_INET, SOCK_STREAM, 0); 114 | if (fd < 0) 115 | { 116 | error("ERROR opening socket"); 117 | } 118 | server = gethostbyname(hostname); 119 | if (server == NULL) 120 | { 121 | fprintf(stderr, "ERROR, no such host\n"); 122 | exit(0); 123 | } 124 | bzero((char *)&serv_addr, sizeof(serv_addr)); 125 | serv_addr.sin_family = AF_INET; 126 | bcopy((char *)server->h_addr, 127 | (char *)&serv_addr.sin_addr.s_addr, 128 | server->h_length); 129 | serv_addr.sin_port = htons(port); 130 | if (connect(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) 131 | { 132 | error("ERROR connecting"); 133 | } 134 | 135 | return fd; 136 | } 137 | 138 | int open_can_socket(const char *port) 139 | { 140 | struct ifreq ifr; 141 | struct sockaddr_can addr; 142 | int soc; 143 | 144 | // open socket 145 | soc = socket(PF_CAN, SOCK_RAW, CAN_RAW); 146 | if(soc < 0) 147 | { 148 | return (-1); 149 | } 150 | 151 | // configure socket 152 | setsockopt(soc, SOL_CAN_RAW, CAN_RAW_RECV_OWN_MSGS, 153 | &LOOPBACK, sizeof(LOOPBACK)); 154 | addr.can_family = AF_CAN; 155 | strcpy(ifr.ifr_name, port); 156 | if (ioctl(soc, SIOCGIFINDEX, &ifr) < 0) 157 | { 158 | return (-1); 159 | } 160 | 161 | addr.can_ifindex = ifr.ifr_ifindex; // why does this have to be after ioctl? 162 | 163 | if (bind(soc, (struct sockaddr *)&addr, sizeof(addr)) < 0) 164 | { 165 | return (-1); 166 | } 167 | 168 | return soc; 169 | } 170 | 171 | int read_frame(int soc,struct can_frame* frame,struct timeval* tv) 172 | { 173 | //TODO: is it worth doing this, or just pass timeval as a separate argument 174 | int bytes; 175 | 176 | bytes = read(soc,frame,sizeof(*frame)); 177 | ioctl(soc, SIOCGSTAMP, tv); 178 | 179 | return bytes; 180 | } 181 | 182 | void* read_poll_can(void* args) 183 | { 184 | int fd = (int)args; 185 | timestamped_frame tf; 186 | struct can_frame frame; 187 | struct timeval tv; 188 | 189 | size_t count = 0; 190 | char* bufpnt = read_buf_can; 191 | const size_t frame_sz = sizeof(tf); 192 | 193 | while(poll) 194 | { 195 | if(count > (BUF_SZ-frame_sz)) 196 | { 197 | //full buffer, drop data and start over. TODO: ring buffer, print/ debug 198 | bufpnt = read_buf_can; 199 | count = 0; 200 | } 201 | 202 | read_frame(fd,&frame,&tv); //blocking 203 | memcpy(bufpnt,(char*)(&tv),sizeof(struct timeval)); 204 | bufpnt += sizeof(struct timeval); 205 | count += sizeof(struct timeval); 206 | memcpy(bufpnt,(char*)(&frame.can_id),sizeof(uint32_t)); 207 | bufpnt += sizeof(uint32_t); 208 | count += sizeof(uint32_t); 209 | memcpy(bufpnt,&(frame.can_dlc),sizeof(uint8_t)); 210 | bufpnt += sizeof(uint8_t); 211 | count += sizeof(uint8_t); 212 | 213 | memcpy(bufpnt,(char*)(&frame.data),sizeof(frame.data)); 214 | bufpnt += sizeof(frame.data); 215 | count += sizeof(frame.data); 216 | 217 | #if DEBUG 218 | printf("message read\n"); 219 | #endif 220 | pthread_mutex_lock(&read_mutex); 221 | if (tcp_ready_to_send) // other thread has said it is able to write to TCP socket 222 | { 223 | socketcan_bytes_available = count; 224 | memcpy(read_buf_tcp,read_buf_can,count); 225 | tcp_ready_to_send = false; 226 | const int signal_rv = pthread_cond_signal(&tcp_send_copied); 227 | if (signal_rv < 0) 228 | { 229 | error("could not signal to other thread.\n"); 230 | } 231 | 232 | bufpnt = read_buf_can; //start filling up buffer again 233 | count = 0; 234 | #if DEBUG 235 | printf("%d bytes copied to TCP buffer.\n",count); 236 | #endif 237 | } 238 | pthread_mutex_unlock(&read_mutex); 239 | } 240 | } 241 | 242 | void* read_poll_tcp(void* args) 243 | { 244 | int tcp_socket = (int)(args); 245 | size_t cpy_socketcan_bytes_available; 246 | int wait_rv; 247 | while(poll) 248 | { 249 | pthread_mutex_lock(&read_mutex); 250 | tcp_ready_to_send = true; 251 | while (!socketcan_bytes_available) 252 | { 253 | wait_rv = pthread_cond_wait(&tcp_send_copied, &read_mutex); 254 | } 255 | if (wait_rv < 0) 256 | { 257 | error("could not resume TCP send thread.\n"); 258 | } 259 | cpy_socketcan_bytes_available = socketcan_bytes_available; // we should only access the original inside a mutex. 260 | socketcan_bytes_available = 0; 261 | pthread_mutex_unlock(&read_mutex); 262 | 263 | // don't want to perform the write inside mutex; 264 | #if DEBUG 265 | printf("ready to send %d bytes\n",cpy_socketcan_bytes_available); 266 | #endif 267 | int n = write(tcp_socket, read_buf_tcp,cpy_socketcan_bytes_available); 268 | if (n < cpy_socketcan_bytes_available) 269 | { 270 | error("failed to sent all bytes over TCP"); 271 | } 272 | #if DEBUG 273 | printf("%d bytes written to TCP\n",n); 274 | timestamped_frame tf; 275 | deserialize_frame(read_buf_tcp,&tf); //TODO: more than one frame. 276 | print_frame(&tf); 277 | #endif 278 | } 279 | } 280 | 281 | void* write_poll(void* args) 282 | { 283 | // CAN write should be quick enough to do in this loop... 284 | can_write_sockets* socks = (can_write_sockets*)args; 285 | struct can_frame frame; 286 | 287 | char write_buf[BUF_SZ]; 288 | char* bufpnt = write_buf; 289 | const size_t frame_sz = 13; // 4 id + 1 dlc + 8 bytes 290 | const size_t can_struct_sz = sizeof(struct can_frame); 291 | 292 | while(poll) 293 | { 294 | int num_bytes_tcp = read(socks->tcp_sock,write_buf,BUF_SZ); 295 | if(num_bytes_tcp == 0) 296 | { 297 | //TODO: don't use error() call handle_signal 298 | error("Socket closed at other end... exiting.\n"); 299 | } 300 | #if DEBUG 301 | printf("%d bytes read from TCP.\n",num_bytes_tcp); 302 | #endif 303 | int num_frames = num_bytes_tcp / frame_sz; 304 | 305 | for(int n = 0;n < num_frames;n++) 306 | { 307 | frame.can_id = ((uint32_t)(*bufpnt) << 0) | ((uint32_t)(*(bufpnt+1)) << 8) | ((uint32_t)(*(bufpnt+2)) << 16) | ((uint32_t)(*(bufpnt+3)) << 24); 308 | frame.can_dlc = (uint8_t)(*(bufpnt+4)); 309 | memcpy(frame.data,bufpnt+5,frame.can_dlc); 310 | #if DEBUG 311 | printf("frame %d | ID: %x | DLC: %d | Data:",n,frame.can_id,frame.can_dlc); 312 | for (int m = 0;m < frame.can_dlc;m++) 313 | { 314 | printf("%02x ",frame.data[m]); 315 | } 316 | printf("\n"); 317 | #endif 318 | int num_bytes_can = write(socks->can_sock, &frame, can_struct_sz); 319 | if (num_bytes_can < can_struct_sz) 320 | { 321 | printf("only send %d bytes of can message.\n",num_bytes_can); 322 | error("failed to send complete CAN message!\n"); 323 | } 324 | bufpnt += frame_sz; 325 | } 326 | bufpnt = write_buf; //reset. 327 | } 328 | } 329 | 330 | void deserialize_frame(char* ptr,timestamped_frame* tf) 331 | { 332 | // tf = (timestamped_frame*)ptr; // doesn't work, struct does some padding. manually populate fields? 333 | size_t count = 0; 334 | memcpy(&(tf -> tv_sec),ptr,sizeof(time_t)); 335 | count += sizeof(time_t); 336 | 337 | memcpy(&(tf -> tv_usec),ptr+count,sizeof(suseconds_t)); 338 | count += sizeof(suseconds_t); 339 | 340 | memcpy(&(tf -> id),ptr+count,sizeof(canid_t)); 341 | count+= sizeof(canid_t); 342 | 343 | memcpy(&(tf -> dlc),ptr+count,sizeof(uint8_t)); 344 | count += sizeof(uint8_t); 345 | 346 | memcpy(tf ->data,ptr+count,tf -> dlc); 347 | } 348 | 349 | void print_frame(timestamped_frame* tf) 350 | { 351 | printf("\t%d.%d: ID %x | DLC %d | Data: ",tf->tv_sec,tf->tv_usec,tf->id,tf->dlc); 352 | for (int n=0;ndlc;n++) 353 | { 354 | printf("%02x ",tf->data[n]); 355 | } 356 | printf("\n"); 357 | } 358 | 359 | int main(int argc, char* argv[]) 360 | { 361 | int port, tcp_socket, can_socket; 362 | char hostname[HOSTNAME_LEN]; 363 | char can_port[HOSTNAME_LEN]; 364 | 365 | pthread_t read_can_thread, read_tcp_thread, write_thread; 366 | 367 | // arg parsing 368 | if (argc < 4) 369 | { 370 | fprintf(stderr, "usage %s can-name hostname port\n", argv[0]); 371 | exit(0); 372 | } 373 | strncpy(can_port, argv[1], HOSTNAME_LEN); 374 | strncpy(hostname, argv[2], HOSTNAME_LEN); 375 | port = atoi(argv[3]); 376 | 377 | // initialising stuff 378 | if (pthread_mutex_init(&read_mutex, NULL) != 0) 379 | { 380 | printf("\n mutex init has failed\n"); 381 | return 1; 382 | } 383 | 384 | can_socket = open_can_socket(can_port); 385 | tcp_socket = create_tcp_socket(hostname, port); 386 | 387 | can_write_sockets write_args = {tcp_socket, can_socket}; 388 | if(pthread_create(&read_can_thread, NULL, read_poll_can, (void*)can_socket) < 0) 389 | { 390 | error("unable to create thread"); 391 | } 392 | if(pthread_create(&read_tcp_thread, NULL, read_poll_tcp, (void*)tcp_socket) < 0) 393 | { 394 | error("unable to create thread"); 395 | } 396 | 397 | if(pthread_create(&write_thread, NULL, write_poll, (void*)(&write_args)) < 0) 398 | { 399 | error("unable to create thread"); 400 | } 401 | pthread_join(read_can_thread,NULL); 402 | pthread_join(read_tcp_thread,NULL); 403 | pthread_join(write_thread,NULL); 404 | return 0; 405 | } -------------------------------------------------------------------------------- /client/makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS = -std=gnu99 3 | LFLAGS = -lpthread 4 | TARGET = client 5 | 6 | all: $(TARGET) 7 | 8 | $(TARGET): $(TARGET).c 9 | $(CC) -o $(TARGET) $(TARGET).c $(CFLAGS) $(LFLAGS) 10 | 11 | clean: 12 | $(RM) *.o *~ $(TARGET) 13 | -------------------------------------------------------------------------------- /examples/log_binary_main.py: -------------------------------------------------------------------------------- 1 | from socketsocketcan import TCPBus 2 | import can 3 | from datetime import datetime 4 | from time import sleep, perf_counter 5 | from queue import Queue 6 | import struct 7 | 8 | bus = TCPBus(5000) 9 | print("socket connected!") 10 | 11 | class BinaryLogger(can.Listener): 12 | """write each message as a 21 byte binary file""" 13 | def __init__(self,filename=None): 14 | if filename is None: 15 | filename = str(datetime.now()) + ".log" 16 | self.filename = filename 17 | self.buffer = Queue() 18 | self._count = 0 19 | self.total_count = 0 20 | 21 | def on_message_received(self,msg): 22 | self.buffer.put(msg) 23 | self._count += 1 24 | self.total_count += 1 25 | if self._count > 100: 26 | self.write_to_file() 27 | self._count = 0 28 | 29 | def on_error(self): 30 | self.write_to_file() 31 | 32 | def write_to_file(self): 33 | with open(self.filename,"ab") as fd: 34 | while not self.buffer.empty(): 35 | msg = self.buffer.get() 36 | sec = int(msg.timestamp) 37 | usec = int((msg.timestamp - sec) * 1e6) 38 | try: 39 | data = msg.data + bytearray(8-msg.dlc) 40 | except: 41 | print("negative size?",msg.dlc,8-msg.dlc) 42 | data = bytearray(8) 43 | fd.write(struct.pack(" 100: 26 | self.write_to_file() 27 | self._count = 0 28 | 29 | def on_error(self): 30 | self.write_to_file() 31 | 32 | def write_to_file(self): 33 | with open(self.filename,"ab") as fd: 34 | while not self.buffer.empty(): 35 | msg = self.buffer.get() 36 | sec = int(msg.timestamp) 37 | usec = int((msg.timestamp - sec) * 1e6) 38 | data = msg.data + bytearray(8-msg.dlc) 39 | fd.write(struct.pack("