Skip to content

Adding an IO Arduino

Kyle Combes edited this page Dec 16, 2017 · 1 revision

One of the killer features of the BOCS is that Arduinos controlling BOCS inputs and outputs can easily be added. Only a few basic requirements need to be satisfied before your new Arduino can simply be plugged into the Raspberry Pi running the BOCS. The requirements are fourfold:

  • Perform a handshake by saying hello to the computer and accepting a hello in return
  • Transmit input messages back to the Raspberry Pi encoded in a standard JSON format
  • Accept string messages sent over serial to control any output
  • Send a "ba-dump" message (a heartbeat) over serial to the computer at least once per 10 seconds

Saying hello

When the BOCS first starts up, it will enter a pairing mode with the Arduinos. The Raspberry Pi will repeatedly transmit Hello from computer until it receives the appropriate response from an Arduino. That response must be in the format Hello from <arduino_name>. The value for <arduino_name> will be what the BOCS uses to identify that Arduino when sending messages, so be sure you use the same name when listing your Arduino in io_states.py (more on that later).

Transmitting input messages

Sending input messages back to the computer is quite straightforward. Simply send a JSON string over serial with an "event_id" and a "data" attribute. Here's an example (remove the newlines in your implementation -- they've just been added here for readability):

   {
      "event_id": "2",
      "data": "3241"
   }

The value for event_id will be the event identifier. It will be used when writing the puzzle to identify which input was triggered when the event callback is fired. You can check which IDs are already being used by viewing the ArduinoCommEventType in raspi/available_io.py. Once you've settled on an unused ID, you should add an alias to yours in the ArduinoCommEventType class definition. This will help to make your puzzle code more readable later.

Accepting messages from the computer

Messages sent from the Raspberry Pi to the Arduinos are not encoded as JSON due to the extremely slow JSON-decoding performance of an Arduino. Rather, messages begin with an identifier character followed by the message "payload" (a string encoding data in any format you care to parse).

The Raspberry Pi code also uses a layer of abstraction between the I/O devices and the raw text that is sent to the Arduinos. For an example, we can look at the KeypadState class definition, which controls the visibility of the numeric keypad.

from raspi.io_states.io_state import IOState


class KeypadState(IOState):

    def __init__(self, visible=False, old_state_json=False):
        IOState.__init__(self, old_state_json)
        self._data['visible'] = visible

    def set_visible(self, is_visible):
        """
        Shows and hides the input to/from the player.
        :param is_visible: True to make the input visible, False to hide it
        """
        self._data['visible'] = is_visible

    def get_arduino_message(self):
        return 'k1' if self._data['visible'] else 'k0'

An instance of this state object is what you would use when writing your puzzle (more on the details of that later). The key part, for this section, is the last bit: the definition of get_arduino_message(). Here, you can see that the Raspberry Pi will be transmitting one of two messages: k1 if the keypad should be visible or k0 if it should be hidden. Here, k is the identifier and 1 is the payload.

Due to the fact that the Arduinos register themselves by their name upon pairing, message identifiers must not be unique across all connected Arduinos. That is, multiple Arduinos can use k as a message identifier because only the intended Arduino will receive the message.

With the k message, the payload is quite simple: 1 for open or 0 for closed. For a more sophisticated payload example, take a look at the Trellis state. There, the payload encodes light configuration, display time, and whether or not the pattern should repeat. It's of the format light_enc1,time1;light_enc2,time2; and so on, where light_enc is the 16-bit binary encoding of which lights should be on and time is the time to keep the lights in that configuration. If there's an R at the end, the pattern repeats.

Sending a heartbeat

This is the easiest part. A l you need to do is transmit "ba-dump" (a heartbeat sound) over serial at least once every 10 seconds (every 3 seconds is a good number). If the computer doesn't receive a message within that timeframe, it will go back into handshake mode. (See the section below for an implementation example.)

Bringing it all together

For an example, here is the code running the keypad, Trellis, and telegraph inputs:

if (handshakeCompleted) {

   // Check for messages from the computer and act on them, if appropriate (main logic here)
   checkSerialForMessages();
 
   // Heartbeat stuff
   unsigned long curTime = millis();

   if (curTime > nextBroadcastTime) {
       Serial.println("ba-dump"); // Send heartbeat message
       nextBroadcastTime += BROADCAST_INTERVAL;
   }

   if (curTime > expectedHeartbeatByTime && usingHandshake) {
       // Haven't heard heartbeat from computer in too long
       handshakeCompleted = false; // Go back into pairing mode
   }

 } else { // We have yet to establish a connection with the computer
   if (Serial.available() > 0) { // The computer is responding
       String msg = Serial.readString();
       handshakeCompleted = msg.equals("Hello from computer\n");
       Serial.println(handshakeCompleted ? "Connected" : "Not connected");
       expectedHeartbeatByTime = millis() + HEARTBEAT_TIMEOUT;
   } else if (millis() > nextBroadcastTime) { // Cry out for a computer
       Serial.println("Hello from arduino_drawer");
       nextBroadcastTime = millis() + BROADCAST_INTERVAL;
   }
}

Clone this wiki locally