Skip to content

bgunson/obd-socketio

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

34 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

obd-socketio

Quickly create a python-socketio server with events corresponding to the python-OBD API. Ideal for controlling an Async connection to a vehicle from a JavaScript socketio client. The module provides default events which correspond to python-OBD methods as well as JSON encoding for its types.

An example use case would be to use the server on a wireless hotspot enabled Raspberry Pi and be able to interact with the connection from another device in a browser. You can use the default events which will behave as python-OBD does or build your own custom websocket/OBD2 API.

Installation

pip install obd-socketio

Basic Usage

import obdio

io = obdio.OBDio()

# Initiate connection to the vehicle
io.connect_obd()

# Create the server socket
io.create_server()  

# Start the server
io.run_server()

API

Connecting to the Vehicle

Use connect_obd() to initiate the serial connection with the vehicle. connect_obd() can be called at anytime during the server's life and can also be used in the body of an event handler.

import obdio

io = obdio.OBDio()    

io.connect_obd('/dev/ttyUSB0')    # connect to specific serial port

See OBD connections for further usage.

Configuring the Server

OBDio currently uses a Uvicorn ASGI (Asynchronous Server Gateway Interface) socketio server for handling events asyncronously. Event handlers must be declared async and emtting events must be called with the await keyword.

Creating a Server

To create and access the socketio server use create_server(**kwargs) which passes its arguments to python-socketio's Server class giving you control over its behaviour as well as engineIO configuration.

import obdio

io = obdio.OBDio()

sio = io.create_server(cors_allowed_origins='*', json=obdio)    # use obdio as the json module for obd support

The json parameter can be substituted for your own or the built-in (import json) module though it cannot serialize some of the obd types.

Running the Server

After creating your server socket you can define as many events or background tasks as needed on top of what is already defined by the OBDio class then call run_server(**config) to listen on your desired host:port.

Notable Parameters:

  • host (str) - Use '0.0.0.0' to be accessible on your LAN.
  • port (int) - the port the server will listen on

More config args can be found on the Uvicorn settings page. Must be called after create_server.

io.create_server()
io.run_server(host='0.0.0.0', port=48484)

Serving Static Files

Use serve_static(files) to confugure the socketio server to serve a files by route, or a folder of static files. See python-socketio server static files for more info.

  • files (dict) - a dictionary of static files to serve by route
io = create_server()    # create the server first

io.server_static({
    '/': 'index.html'   # send some index for the route '/'
})

io.run_server()     # run the server after defining the static files

The Watch Callback

When you create an OBDio instance, every OBD command added to the watch loop will be assigned a placeholder callback function called watch_callback. It is left for you to define so that data can be handled in your program uniquely. For example, for each response the value could be cached into a dictionary then that entire object streamed over the socket at a lesser rate than watch_callback may be fired.

Your custom watch callback will be passed an OBDResponse. If you use obdio as your socket JSON serializer, the responses will be encoded correctly when you emit back to the client.

  • response (obd.OBDResponse) - the obd value that triggered the callback
data = {}
def cache_values(response):
    data[response.command.name] = response 

io.watch_callback = cache_values

Events

When you create and run an OBDio server events are defined for the public python-OBD API which itself is powerful. However based on your use case you may want to add more functionality to the server beyond what is defined. Also, you may wish to override the default implementation of the predefined API to suit your needs.

Creating Events

One way to create custom or override events is to use create_event(name, handler).

  • name (string) - the name of a custom or overriden event
  • async handler(sid, data) (function) - the custom event handler - must be async

You are able to create custom events, or override the defaults with custom behaivour.

sio = io.create_server()   # call create server first to access the socket

async def custom_handler(sid, data):    # handlers must be async
    await sio.emit('custom', data)      # any emits must be awaited

io.create_event('custom_event', custom_handler)

io.run_server(port=3000)     # lastly call run_server

Or, you can use the @sio.event decorator allowing you to create events as you would with an ASGI server.

import obd

""" Override the default watch event """
@sio.event
async def watch(sid, cmd):
    io.connection.watch(obd.commands[cmd])    # access python-OBD connection and methods
    await sio.emit('event')     # emits must be awaited

io.run_server(port=48484)

Default Server Events

On creation of an OBDio server most of the python-OBD API is exposed through events of the same name as its functions. The server will handle the event then respond with data (if any) using the same event name.

Event Name Argument Type Response Type
'status' None string
'is_connected' None boolean
'protocol_name' None string
'protocol_id' None string
'port_name' None string
'supports' string1 boolean
'supported_commands' None object
'query' 2 string1 N/A
'start' None N/A
'watch' string[]1 N/A
'unwatch' string[]1 N/A
'unwatch_all' None N/A
'has_name' string1 boolean
'close' None null
  1. Arg is a list of OBD commands by name i.e. 'RPM', 'SPEED'. See the OBD Command Tables.

  2. To query a command, it must be watched first

See the python-OBD documentation for expected behaviour of the default events.

JSON Encodings

Currently the JSON encodings for the python-OBD types provide minimal data excluding unnessacary and non-human readable values to keep payload size smaller when emitting back to a client.

OBDCommand

  • name (string) - human readable representation of a PID, all caps
  • desc (string) - human readable description of the command

Example:

{
    "name": "SPEED",
    "desc": "Vehicle Speed"
}

OBDResponse

  • value (any) - the response value: numeric, array
  • command (OBDCommand) - the command the response is for
  • time (number) - Unix timestamp (seconds) of the response
  • unit (string) - the unit of the response value. Non-numeric responses (dtcs, o2 sensors, etc) units will be represented as their type such as "<class 'tuple'>" while numeric ones will be a literal representing their unit from the obd unit registry (pint value).

Example:

{
    "value": 1500,
    "command": {
        "name": "RPM",
        "desc": "Engine RPM"
    },
    "time": 1639266637000,  // epoch time in ms
    "unit": "revolutions_per_minute"
}

OBDResponse - Status Example:

{
    "value": {
        "MIL": false,
        "DTC_COUNT": 0,
        "ignition_type": "spark"
    },
    "command": {
        "name": "STATUS",
        "desc": "Status since DTCs cleared"
    },
    "time": 1639341509038,
    "unit": "<class 'obd.OBDResponse.Status'>"
}

Note the value is another object and unit describes the type of the response since it is not measurable.

Related