mirror of
https://github.com/andreili/katapult.git
synced 2025-08-23 19:34:06 +02:00
pycanserial: can serial test module rewritten in python
Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
2762299895
commit
927f603e60
11
scripts/canserial.service
Normal file
11
scripts/canserial.service
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Serial over CAN emulator
|
||||||
|
After=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=idle
|
||||||
|
User=pi
|
||||||
|
ExecStart=python3 /home/pi/CanBoot/scripts/pycanserial.py
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
305
scripts/pycanserial.py
Normal file
305
scripts/pycanserial.py
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# Python 3 port of "CanSerial"
|
||||||
|
#
|
||||||
|
# Copyright (C) 2021 Eric Callahan <arksine.code@gmail.com>
|
||||||
|
#
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||||
|
import asyncio
|
||||||
|
import socket
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import pty
|
||||||
|
import termios
|
||||||
|
import fcntl
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import errno
|
||||||
|
|
||||||
|
CAN_FMT = "<IB3x8s"
|
||||||
|
CANBUS_ID_UUID = 0x321
|
||||||
|
CANBUS_ID_SET = 0x322
|
||||||
|
CANBUS_ID_UUID_RESP = 0x323
|
||||||
|
CANBUS_UUID_LEN = 6
|
||||||
|
CANBUS_PORT_OFFSET = 0x180
|
||||||
|
UUID_CONFIG_FILE = "/var/tmp/canuuids.cfg"
|
||||||
|
UPDATE_REQUEST_TIME = 5.
|
||||||
|
|
||||||
|
class CanSerialError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class PeriodicCallback:
|
||||||
|
def __init__(self, period, callback, *args):
|
||||||
|
try:
|
||||||
|
self.aio_loop = asyncio.get_running_loop()
|
||||||
|
except RuntimeError:
|
||||||
|
self.aio_loop = asyncio.get_event_loop()
|
||||||
|
self.running = False
|
||||||
|
self.cb = callback
|
||||||
|
self.period = period
|
||||||
|
self.args = args
|
||||||
|
self.timer_hdl = None
|
||||||
|
|
||||||
|
def _create_cb_task(self):
|
||||||
|
asyncio.create_task(self._cb_wrapper())
|
||||||
|
|
||||||
|
async def _cb_wrapper(self):
|
||||||
|
ret = self.cb(*self.args)
|
||||||
|
if asyncio.iscoroutine(ret):
|
||||||
|
await ret
|
||||||
|
if self.running:
|
||||||
|
self.timer_hdl = self.aio_loop.call_later(
|
||||||
|
self.period, self._create_cb_task)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.running = True
|
||||||
|
self.timer_hdl = self.aio_loop.call_later(
|
||||||
|
self.period, self._create_cb_task)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.running = False
|
||||||
|
if self.timer_hdl is not None:
|
||||||
|
self.timer_hdl.cancel()
|
||||||
|
self.timer_hdl = None
|
||||||
|
|
||||||
|
class CanDeviceLink:
|
||||||
|
def __init__(self, canserial, uuid_bytes, can_id):
|
||||||
|
self.canserial = canserial
|
||||||
|
self.aio_loop = canserial.aio_loop
|
||||||
|
self.uuid_bytes = uuid_bytes
|
||||||
|
self.uuid_str = "".join([f"{b:02x}" for b in self.uuid_bytes])
|
||||||
|
self.can_id = can_id
|
||||||
|
self.last_recd_time = 0
|
||||||
|
self.ping_tries = 0
|
||||||
|
self.pty_fd = None
|
||||||
|
self._create_pty()
|
||||||
|
# Send ID Back after creation
|
||||||
|
self.respond_id_request()
|
||||||
|
|
||||||
|
def _create_pty(self):
|
||||||
|
ptyname = f"/tmp/ttyCAN0_{self.uuid_str}"
|
||||||
|
try:
|
||||||
|
os.unlink(ptyname)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
mfd, sfd = pty.openpty()
|
||||||
|
fname = os.ttyname(sfd)
|
||||||
|
os.chmod(fname, 0o660)
|
||||||
|
os.symlink(fname, ptyname)
|
||||||
|
attrs = termios.tcgetattr(mfd)
|
||||||
|
attrs[3] = attrs[3] & ~termios.ECHO
|
||||||
|
termios.tcsetattr(mfd, termios.TCSADRAIN, attrs)
|
||||||
|
fl = fcntl.fcntl(mfd, fcntl.F_GETFL) | os.O_NONBLOCK
|
||||||
|
fcntl.fcntl(mfd, fcntl.F_SETFL, fl)
|
||||||
|
self.aio_loop.add_reader(mfd, self._handle_pty_data)
|
||||||
|
self.pty_fd = mfd
|
||||||
|
|
||||||
|
def _handle_pty_data(self):
|
||||||
|
try:
|
||||||
|
data = os.read(self.pty_fd, 4096)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Error reading pty")
|
||||||
|
return
|
||||||
|
self.canserial.can_send(self.can_id, data)
|
||||||
|
|
||||||
|
def ping(self):
|
||||||
|
time_diff = self.aio_loop.time() - self.last_recd_time
|
||||||
|
if time_diff < UPDATE_REQUEST_TIME + 1.:
|
||||||
|
# No need to ping as we are receiving data
|
||||||
|
# from the Can Device
|
||||||
|
return
|
||||||
|
if self.ping_tries > 3:
|
||||||
|
logging.info(f"Connection Timed Out: {self.uuid_str}")
|
||||||
|
self.close()
|
||||||
|
return
|
||||||
|
self.ping_tries += 1
|
||||||
|
self.canserial.can_send(self.can_id)
|
||||||
|
|
||||||
|
def handle_can_data(self, data):
|
||||||
|
# TODO: we may need to make sure something is connected
|
||||||
|
# to the other end of the pty. We shouldn't write to
|
||||||
|
# it otherwise.
|
||||||
|
|
||||||
|
self.last_recd_time = self.aio_loop.time()
|
||||||
|
if not data:
|
||||||
|
# This is pong
|
||||||
|
self.ping_tries = 0
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
os.write(self.pty_fd, data)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Error writing to pty")
|
||||||
|
|
||||||
|
def respond_id_request(self):
|
||||||
|
self.last_recd_time = self.aio_loop.time()
|
||||||
|
can_id_bytes = struct.pack("<H", self.can_id)
|
||||||
|
response = can_id_bytes + self.uuid_bytes
|
||||||
|
self.canserial.can_send(CANBUS_ID_SET, response)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.canserial.remove_device(self.can_id)
|
||||||
|
self.aio_loop.remove_reader(self.pty_fd)
|
||||||
|
try:
|
||||||
|
os.close(self.pty_fd)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CanSerial:
|
||||||
|
def __init__(self):
|
||||||
|
self.aio_loop = asyncio.get_event_loop()
|
||||||
|
self.cansock = socket.socket(socket.PF_CAN, socket.SOCK_RAW,
|
||||||
|
socket.CAN_RAW)
|
||||||
|
self.device_links = {}
|
||||||
|
self.can_response_cbs = {CANBUS_ID_UUID_RESP: self._handle_set_id}
|
||||||
|
self.can_status_cb = PeriodicCallback(UPDATE_REQUEST_TIME,
|
||||||
|
self._request_can_status)
|
||||||
|
self.input_buffer = b""
|
||||||
|
self.output_packets = []
|
||||||
|
self.input_busy = False
|
||||||
|
self.output_busy = False
|
||||||
|
with open(UUID_CONFIG_FILE, 'r') as f:
|
||||||
|
data = f.read()
|
||||||
|
lines = [line.strip() for line in data.split('\n')
|
||||||
|
if line.strip() and line.strip()[0] != "#"]
|
||||||
|
self.mapped_can_devs = {}
|
||||||
|
for line in lines:
|
||||||
|
port, uuid = line.split()
|
||||||
|
can_id = 2 * int(port) + CANBUS_PORT_OFFSET
|
||||||
|
self.mapped_can_devs[uuid] = can_id
|
||||||
|
|
||||||
|
def remove_device(self, can_id):
|
||||||
|
self.can_response_cbs.pop(can_id + 1)
|
||||||
|
self.device_links.pop(can_id)
|
||||||
|
|
||||||
|
def _handle_can_response(self):
|
||||||
|
try:
|
||||||
|
data = self.cansock.recv(4096)
|
||||||
|
except socket.error as e:
|
||||||
|
# If bad file descriptor allow connection to be
|
||||||
|
# closed by the data check
|
||||||
|
if e.errno == errno.EBADF:
|
||||||
|
logging.exception("Can Socket Read Error, closing")
|
||||||
|
data = b''
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
if not data:
|
||||||
|
# socket closed
|
||||||
|
self.close()
|
||||||
|
return
|
||||||
|
self.input_buffer += data
|
||||||
|
if self.input_busy:
|
||||||
|
return
|
||||||
|
self.input_busy = True
|
||||||
|
while len(self.input_buffer) >= 16:
|
||||||
|
packet = self.input_buffer[:16]
|
||||||
|
self._process_packet(packet)
|
||||||
|
self.input_buffer = self.input_buffer[16:]
|
||||||
|
self.input_busy = False
|
||||||
|
|
||||||
|
def _process_packet(self, packet):
|
||||||
|
can_id, length, data = struct.unpack(CAN_FMT, packet)
|
||||||
|
can_id &= socket.CAN_EFF_MASK
|
||||||
|
payload = data[:length]
|
||||||
|
canfunc = self.can_response_cbs.get(can_id)
|
||||||
|
if canfunc is None:
|
||||||
|
logging.info(f"No response callback for CAN ID {can_id:3X}'")
|
||||||
|
return
|
||||||
|
canfunc(payload)
|
||||||
|
|
||||||
|
def _handle_set_id(self, uuid_bytes):
|
||||||
|
if len(uuid_bytes) != CANBUS_UUID_LEN:
|
||||||
|
logging.info(f"Invalid length for UUID: {uuid_bytes}")
|
||||||
|
return
|
||||||
|
uuid = ":".join([f"{b:02X}" for b in uuid_bytes])
|
||||||
|
can_id = None
|
||||||
|
if uuid in self.mapped_can_devs:
|
||||||
|
can_id = self.mapped_can_devs[uuid]
|
||||||
|
if can_id in self.device_links:
|
||||||
|
self.device_links[can_id].respond_id_request()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
can_id = self._assign_can_port(uuid)
|
||||||
|
new_dev = CanDeviceLink(self, uuid_bytes, can_id)
|
||||||
|
self.device_links[can_id] = new_dev
|
||||||
|
self.can_response_cbs[can_id + 1] = new_dev.handle_can_data
|
||||||
|
|
||||||
|
def _assign_can_port(self, uuid):
|
||||||
|
port = len(self.mapped_can_devs) + 1
|
||||||
|
with open(UUID_CONFIG_FILE, "a") as f:
|
||||||
|
f.write(f"{port} {uuid}\n")
|
||||||
|
can_id = 2 * port + CANBUS_PORT_OFFSET
|
||||||
|
self.mapped_can_devs[uuid] = can_id
|
||||||
|
return can_id
|
||||||
|
|
||||||
|
def _request_can_status(self):
|
||||||
|
# See Check for new devices
|
||||||
|
self.can_send(CANBUS_ID_UUID)
|
||||||
|
# Ping Each device
|
||||||
|
for dev in list(self.device_links.values()):
|
||||||
|
dev.ping()
|
||||||
|
|
||||||
|
def can_send(self, can_id, payload=b""):
|
||||||
|
if can_id > 0x7FF:
|
||||||
|
can_id |= socket.CAN_EFF_FLAG
|
||||||
|
if not payload:
|
||||||
|
packet = struct.pack(CAN_FMT, can_id, 0, b"")
|
||||||
|
self.output_packets.append(packet)
|
||||||
|
else:
|
||||||
|
while payload:
|
||||||
|
length = min(len(payload), 8)
|
||||||
|
pkt_data = payload[:length]
|
||||||
|
payload = payload[length:]
|
||||||
|
packet = struct.pack(
|
||||||
|
CAN_FMT, can_id, length, pkt_data)
|
||||||
|
self.output_packets.append(packet)
|
||||||
|
if self.output_busy:
|
||||||
|
return
|
||||||
|
self.output_busy = True
|
||||||
|
asyncio.create_task(self._do_can_send())
|
||||||
|
|
||||||
|
async def _do_can_send(self):
|
||||||
|
while self.output_packets:
|
||||||
|
packet = self.output_packets.pop(0)
|
||||||
|
try:
|
||||||
|
await self.aio_loop.sock_sendall(self.cansock, packet)
|
||||||
|
except socket.error:
|
||||||
|
logging.info("Socket Write Error, closing")
|
||||||
|
self.close()
|
||||||
|
break
|
||||||
|
self.output_busy = False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
try:
|
||||||
|
self.cansock.bind(("can0",))
|
||||||
|
except Exception:
|
||||||
|
raise CanSerialError("Unable to bind socket to can0")
|
||||||
|
self.cansock.setblocking(False)
|
||||||
|
self.aio_loop.add_reader(
|
||||||
|
self.cansock.fileno(), self._handle_can_response)
|
||||||
|
self.can_status_cb.start()
|
||||||
|
self.aio_loop.run_forever()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.can_status_cb.stop()
|
||||||
|
self.aio_loop.remove_reader(self.cansock.fileno())
|
||||||
|
for dev in list(self.device_links.values()):
|
||||||
|
dev.close()
|
||||||
|
self.aio_loop.stop()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
canserial = CanSerial()
|
||||||
|
canserial.start()
|
||||||
|
except Exception as e:
|
||||||
|
logging.exception(str(e))
|
||||||
|
time.sleep(5.)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
# Initialize UUID file if it doesn't exist
|
||||||
|
if not os.path.exists(UUID_CONFIG_FILE):
|
||||||
|
with open(UUID_CONFIG_FILE, "w") as f:
|
||||||
|
f.write("# [port] [UUID]\n")
|
||||||
|
main()
|
Loading…
x
Reference in New Issue
Block a user