Sunday, February 17, 2019

Hacking my Volt with a Raspberry Pi - Part 4: Software

Code repositories

Since I started this series, I have had many requests for the code behind it. A majority of the code is on Gitlab already:

Some of the other code (like the Android app) I cannot release yet because it's intertwined with a bunch of other irrelevant code (like my home automation system). I may release this code in the future at some point.

Overview of services on the Raspberry Pi

Serial Monitor

serial_monitor.py

This is the primary service that interfaces with the Macchina M2 and the phone. It is responsible for decoding frames from the serial port, connecting to the phone app via Bluetooth, and receiving and routing messages between the 3 systems. It is also responsible for managing the GPIO poller process (gpio_poll.c). It opens a UDP socket bound to the loopback interface that it receives messages on. There are several other services that send it commands this way.

monitor_hotload.py

This is loaded as a module by serial_monitor.py, which defers quite a bit of logic to it. It is a bit unusual in that most of the functions defined in it have a 'self' argument, even though they are not actually part of a class. These are treated as "methods" of the SerialMonitor class in serial_monitor.py. The reason it is done this way is so that it can be easily reloaded on the fly, as seen in my demonstration video.

This module is responsible for:

  • Layout of the display
  • Parsing and interpreting data frames and events from Macchina
  • Responding to button press events (from gpio_poll)
  • Data logging (cardata and managing can bus dumper)
  • Idle queries - sends an info packet to my server every 10 minutes while the car is off (replaces OnStar Vehicle Status)
  • Keeping the system clock updated based on clock frame from vehicle
  • Controls power to the screen
  • Controls the beeper (via gpio_poll)
gpio_poll.c

This C program polls the GPIO pins for the buttons. It maps the registers directly so that it can control GPIO without going through the kernel. It also sets up the internal pull-up resistors for the button GPIOs, and controls the PWM output for the beeper. The six buttons are connected to 3 GPIO pins (A, B, and C) and ground (G). The buttons are connected to the following pins:

  • B & G - White
  • A & B - Black
  • A & G - Dial
  • A & C - Blue
  • B & C - Green
  • C & G - Red

To poll the buttons, it goes through 4 cycles with a delay of 5ms in between: one with all of the pins in input mode, one with A pulled low, one with B pulled low, and one with C pulled low. From this, it can determine when any one of the six buttons is pressed. Unfortunately, it cannot detect more than one button being pressed at the same time, but that hasn't been a problem. Whenever a button is pressed or released, an event is sent over a pipe to serial_monitor.py. This process also detects "long presses" (250ms).

The rotor is also polled at the same time through its two dedicated pins. There are 4 codes it can return (in Gray code) which is repeated 6 times around the dial for a total of 24 pulses per rotation.

The beeper is controlled through a ring buffer in shared memory consisting of (frequency, duration) pairs. At the end of the buffer, or when frequency = 0, then the beeper is turned off.

canlog_shmem.c

This program is optional, and runs as two processes: In "source" mode, it connects to the Macchina via USB and tells it to forward every CAN frame it recieves. It then reads those frames from the Macchina and places them in a shared-memory buffer. In "sink" mode, it reads from this shared buffer, and writes the frames to a compressed file. This ensures that even if the I/O to the SD card stalls for whatever reason, or the compression takes too long, then the source process can still read frames from USB and doesn't get backlogged - it simply dropps frames that can't go into the ring buffer.

hud_shm.py

This is a helper module which contains the necessary data structures for communicating with the HUD service.

bitstream/*, cardata_shmem.py

This is an auxilliary Python extension to assist with parsing data frames from the Macchina. It is not strictly necessary, since the data can be parsed in Python code, but it is more efficient to do it here.

connect_spp.sh

This is a helper script which determines the the RFCOMM channel that the phone is listening to the SPP service on, then associates rfcomm0 with the phone.

Dashcam Monitor

dashcam_monitor.py

This service is responsible for managing the dashcams. It is state-machine driven, transitioning between states based on the presence of flag files. The states all have 3-letter codes which are shown in the corner of the display:

  • IDL (IdleState) - Default state, cameras are off.
  • REC (RecordState) - Cameras are powered on and recording.
  • PWR (PoweroffWaitState) - Cameras are being powered off, and the monitor is waiting for the capacitors to drain.
  • WFS (WifiWaitState) - Cameras possibly have new videos, waiting to connect to home Wifi network.
  • USB (WaitUSBState) - Cameras and hub are powered on, waiting for cameras to go into storage mode and show up in /dev/disk/by-label.
  • MNT (MountState) - One of the cameras is being mounted.
  • CPY (RunCopyState) - Videos are being copied from camera.
  • UMT (UnmountState) - Camera is being unmounted.
  • MAN (ManualMountState) - User requested manual poweron and mount (want-mount).
  • HUB (ResetHubState) - User requested hub be turned off for 2 seconds to reset it. This is necessary because sometimes the cameras don't show up over USB.

There are several flag files that control the state machine:

  • want-record - Something wants the cameras to record. Causes IdleState to transition to RecordState, and causes RunCopyState to abort any copy in progress.
  • need-check - Set whenever the camera has been powered on in record mode, indicating that there may be new videos. Causes IdleState to transition to WifiWaitState
  • want-lcopy - Indicates the user wants to do a local copy.
  • abort-all - Causes the current operation to be aborted and returns the monitor to IdleState.
  • copy-restart - Causes the current copy operation to be restarted.
  • reset-hub - Causes the USB Hub to be reset.
do_copy.py

This script is responsible for actually copying videos to my servers. It gathers metadata about each video, determines which videos are contiguous, then sends metadata to the server, which responds with which files still need to be uploaded. It then uploads each video in chunks of 2MB at a time.

do_mount.sh, do_unmount.sh

These scripts are responsible for actually mounting the cameras. One quirk of the A119S camera I use in the front is that it will start recording for a second even when going into storage mode. This leaves a lot of very short videos on the card unless they are deleted. The do_mount.sh script deletes these files, then remounts the card read-only so that even if power is lost to the camera while copying, it won't damage the filesystem.

HUD

hud.c

This program is responsible for actually drawing text on the screen. It receives layout instructions through shared memory, and updates text asynchronously. It uses Cairo to do the drawing to a memoroy buffer, then copies changed portions of that buffer directly to the Raspberry Pi's framebuffer. This is more efficient than running an instance of X.org.

Wifi monitor

wifi_monitor.py

This service nudges the Wifi interface to "encourage" it to try its hardest to connect whenever we have videos we want to upload. It does this by pinging the upload host, and if it can't be reached, it will call wpa_cli scan, and if that doesn't work, it will remove and reinsert the driver for the interface. It reports its status back to dashcam-monitor by writing the local address to wifi-addr.

Hologram monitor

hologram_monitor.py

This service keeps PPPD running whenever the Hologram Nova modem is plugged in. It also maintains the alternate routing table, which ensures that sockets only use the modem when the specifically bind to its address. This prevents background services from consuming data over Hologram, which is expensive.

ppp-chat-script

This file is used by pppd to set up the modem to connect.

Hologram Command service

hologram_command_listener.py

This service listens for command packets on the Hologram interface, using port 4011. The only way to send packets to this address is to use the Hologram REST API. The commands are verified using HMAC-SHA256 and are timestamped to protect against replay attacks.

Log service

logmgr.py

This service receives the output from all the other services and saves it to a log file.

RFCOMM Listen service

rfcomm_read_commands.sh, run_rfcomm_listen.sh

I haven't actually run this service since setting up Hologram - it is intended to recieve commands over bluetooth, but it wasn't secure since it would accept connections from any device.

Support modules

utils.py

This module contains common functions and classes used by most of the Python services

hotload.py

This module contains the code to detect live changes to the source code of a module and reload it.

Service management

run_*.sh

Any shell script starting with run_ is simply a wrapper that runs the corresponding service and ensures its output is connected to the Log manager service.

etc/*.server

These are systemd unit files for each of the services that needs to run.

etc/install

This script installs the .service files into /etc/systemd after updating the paths in them to point to the correct location.

Utility scripts

obd.sh

This script causes the Macchina to send an OBD2 query, then it prints the result.

send_command.py

This script does not run on the Pi. It is used to send a command to the Pi using the Hologram API.

log_receiver.py

This script runs on my server, and receives and logs the info packets sent by the Pi.

update_cardata_fields.py

This contains the master list of cardata columns, how many bits each one takes in the frame, and whether the value is signed. It auto updates all the relevant sections of the parser and generator in each source file so everything stays in sync.

Macchina M2 firmware

bt_interface.cpp

This file contains most of the custom code I have written for the Macchina. It contains the code to extract data from received CAN frames, communicate with the Pi via the serial port on the back, and communicate directly with the phone if a bluetooth module is present (it currently isn't). It also sends OBD2 queries in a specific sequence designed to sample all PIDs at even intervals, with more important PIDs being sampled more often.

calc_pids.py

This script contains the master list of PIDs to query, as well as the sequence in which they are queried. It is organized as a tree - the top level has 5 elements, and so time is divided equally 5 ways between each subtree. The first 3 elements are dedicated to sampling the amperage of the HV battery and the two motors, which consumes 3/5 of all queries sent. The remaining 2/5 is divided between voltage, range remaining, temperatures, and trouble code queries.

obd2_query_sequence.h, obd2_switch.h

These files are generated by calc_pids.py.

update_m2ret_cardata_fields.py

This script useds the master list from update_cardata_fields.py to update bt_interface.cpp with the current list of fields.

Other software

There are a few other scripts and programs that interface with the system or otherwise use the data, but I haven't had time to organize it. I may go into more detail in a future post.

No comments: