Python 3.9: Now with SAE J1939 socket support

I implemented support in the socket module for SAE J1939 sockets, which is now available in Python 3.9! It's present in all builds after (and including) 3.9.0 beta 1.

J1939 is a higher-level protocol built using CAN as the physical layer, and is typically seen in heavy-duty trucks and agriculture equipment. It uses 29-bit extended message IDs and introduces the notion of node IDs and address claiming, which allows for point-to-point and broadcast semantics as defined in the protocol. You can read more about J1939 in the page from Copperhill Technologies or the official SAE J1939 standards if you prefer more technical documents.

The J1939 patches were originally developed out of tree and maintained by the linux-can mailing list, but were finally upstreamed and part of the 5.4 kernel release. The rationale for having a kernel implementation (as opposed to one purely in userspace) is covered in the Linux kernel documentation.

Hopefully anyone who might work with SAE J1939 finds this useful! If you're interested in working with J1939 in Python 3.9, I recommend trying out the beta so any bugs can be addressed before the final release in October per PEP 596.

# Using the deadsnakes PPA
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt-get update
sudo apt-get install python3.9

Here's a quick sample program that implements part of testj1939 as seen in the J1939 Kickstart guide:

import socket

def main():
    with socket.socket(
        family=socket.PF_CAN, type=socket.SOCK_DGRAM, proto=socket.CAN_J1939
    ) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
        addr = "vcan0", socket.J1939_NO_NAME, socket.J1939_NO_PGN, socket.J1939_NO_ADDR

        while True:
            data, addr = s.recvfrom(128)
            print("{:02x} {:05x}:".format(addr[3], addr[2]), end="")

            for j in range(len(data)):
                if j % 8 == 0 and j != 0:
                    print("\n{:05x}    ".format(j), end="")
                print(" {:02x}".format(data[j]), end="")
            print("\n", end="")

if __name__ == "__main__":