Skip to content

Commit

Permalink
train demo - firmware for signals
Browse files Browse the repository at this point in the history
  • Loading branch information
briandorsey committed Apr 11, 2024
1 parent 521fba4 commit ef9641f
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 0 deletions.
31 changes: 31 additions & 0 deletions other/train-to-cloud-city/devices/signal/CIRCUITPY/boot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import time
import board
import neopixel
import touchio

print("hello from boot.py")



import usb_cdc
usb_cdc.enable(data=True, console=False)

import usb_midi
usb_midi.disable()

import usb_hid
usb_hid.disable()
154 changes: 154 additions & 0 deletions other/train-to-cloud-city/devices/signal/CIRCUITPY/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Requirements
#
# Download stable version 9 of the CircuitPython runtime:
# https://circuitpython.org/board/adafruit_qtpy_rp2040/
# then install via boot mode:
# https://learn.adafruit.com/welcome-to-circuitpython/installing-circuitpython

print("Hello Trains!")

import board
import time
import neopixel
import usb_cdc
import asyncio

VERSION = '3'


# Colors are (Red, Green, Blue) on a 0-255 scale.
OFF = ((0, 0, 0, 0), (0, 0, 0, 0), (0, 0, 0, 0))
ORANGE = ((25, 15, 0, 0), (250, 150, 0, 0), (250, 150, 0, 0))
STOP = ((25, 0, 0, 0), (250, 0, 0, 0), (250, 0, 0, 0))
CLEAR = ((0, 25, 0, 0), (0, 250, 0, 0), (0, 250, 0, 0))

HELP = b"""HELP
Valid commands:
OFF Turn off signal light.
STOP Set signal light to red.
CLEAR Set signal light to green.
HELP This text.
(follow each command with a newline character)"""

BRIGHTNESS = 0.9
onboard_pixel = neopixel.NeoPixel(board.NEOPIXEL, 1,
brightness=BRIGHTNESS)

pixel_pin = board.A0
num_pixels = 2
pixels = neopixel.NeoPixel(pixel_pin, num_pixels,
brightness=BRIGHTNESS, auto_write=False, pixel_order=(1, 0, 2, 3),)

# This signal's ID. Write some short string to "id.txt"
try:
ID = open("id.txt").read().strip().encode("UTF-8")
except BaseException:
ID = b"UNKNOWN"

board.BUTTON
from digitalio import DigitalInOut, Direction, Pull

btn = DigitalInOut(board.BUTTON)
btn.direction = Direction.INPUT
btn.pull = Pull.UP


def init():
set_pixels(ORANGE)
print(f'my id is: {ID}')
usb_cdc.data.write(ID + b':INIT\n')
usb_cdc.data.write(ID + b':VERSION ' + VERSION.encode('UTF-8') + '\n')

def set_pixels(color_rgb):
onboard_pixel[0] = color_rgb[0][:3]
pixels[0] = color_rgb[1]
pixels[1] = color_rgb[2]
pixels.show()

def serial_response(result):
usb_cdc.data.write(ID)
usb_cdc.data.write(b':')
usb_cdc.data.write(result)
usb_cdc.data.write(b'\n')

def parse_and_run_command(input):
input = input.strip().upper()
if input == b'HELP':
set_pixels(ORANGE)
return HELP
elif input == b'OFF' or input == b'O':
set_pixels(OFF)
return input
elif input == b'STOP' or input == b'S':
set_pixels(STOP)
return input
elif input == b'CLEAR' or input == b'C':
set_pixels(CLEAR)
return input
else:
set_pixels(ORANGE)
return b'ERROR: Unknown command: ' + input + b' (Enter HELP for command list.)'

async def usb_client():
usb_cdc.data.timeout = 0
s = asyncio.StreamReader(usb_cdc.data)
while True:
data = await s.readline()
print("input: ", data)
try:
result = parse_and_run_command(data)
except Exception as err:
result = 'ERROR: ' + str(type(err)) + str(err)
result.encode("UTF-8")
serial_response(result)

# Press "BOOT" button to cycle between states.
async def button():
prev_state = btn.value
states = [(STOP, b'STOP'), (CLEAR, b'CLEAR'), (OFF, b'OFF'), (ORANGE, b'ERROR')]
index = 0

while True:
cur_state = btn.value
if cur_state != prev_state:
if not cur_state:
# print("BUTTON is down")
pass
else:
# print("BUTTON is up")
color, result = states[index % 4]
set_pixels(color)
serial_response(result)
index += 1
prev_state = cur_state
await asyncio.sleep(0.2)

async def counter():
i = 0
while True:
usb_cdc.data.write(ID)
usb_cdc.data.write(b':HEARTBEAT\n')
await asyncio.sleep(60)

async def main():
init()
clients = [asyncio.create_task(counter())]
clients.append(asyncio.create_task(usb_client()))
clients.append(asyncio.create_task(button()))
await asyncio.gather(*clients)

asyncio.run(main())
1 change: 1 addition & 0 deletions other/train-to-cloud-city/devices/signal/CIRCUITPY/id.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SIGNAL_A
52 changes: 52 additions & 0 deletions other/train-to-cloud-city/devices/signal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
This is the code which runs on each of the physical signal lights.

For the demo, they were build out of lego and an Adafruit [QT Py RP2040](https://www.adafruit.com/product/4900) and two [NeoPixel RGBW Mini Button](https://www.adafruit.com/product/4776)s.

It's possible to test without the extra NeoPixels, the code will also update the NeoPixel on the QT Py board.

## Installation

Download stable version 9 of the CircuitPython runtime:
https://circuitpython.org/board/adafruit_qtpy_rp2040/

and the libraries bundle version 9:
https://circuitpython.org/libraries

then install via boot mode:
https://learn.adafruit.com/welcome-to-circuitpython/installing-circuitpython


## Installation steps

* Copy the *.uf2 file to the RPI-RP2 volume. It will then reboot into a volume nameed `CIRCUITPY`.
* Then copy all files in the `CIRCUITPY` directory onto the new USB media volume which should be named `CIRCUITPY`.
* Then set the ID for this device by editing `id.txt`.
* Copy needed library modules from the libraries bundle to the `lib` directory on the USB volume: `neopixel_spi.mpy`, `neopixel.mpy`, `adafruit_ticks.mpy` and the `asyncio` folder.
* Then eject and reconnect. Should be able to connect with a serial terminal program at 9600 baud. (CoolTerm works well on MacOS)


## Protocol

The signal is lit yellow at startup and on errors, green for CLEAR and red for STOP.

Each signal exposes a USB serial port and accepts commands via a text protocol.

```plain
Valid commands:
OFF Turn off signal light.
STOP Set signal light to red.
CLEAR Set signal light to green.
HELP This text.
(follow each command with a newline character)
```
And it responds with its' own signal ID and the executed command, or error text:
```plain
SIGNAL_A:HEARTBEAT
SIGNAL_A:CLEAR
SIGNAL_A:STOP
SIGNAL_A:CLEAR
SIGNAL_A:STOP
SIGNAL_A:ERROR: Unknown command: ABC (Enter HELP for command list.)
```

It also sends `SIGNAL_A:HEARTBEAT` approximately once a minute.

0 comments on commit ef9641f

Please sign in to comment.