[{"element_type":"code","content":"# Next MBTA Ride Sign\n# 2025 Paul M\n# SPDX-License-Identifier: MIT\n\n# Creates a sign that shows when the next bus or subway train will arrive at an MBTA stop\n# Stops can be commuter rail stops, subway stations, or bus stops\n\nimport time\nimport board\nimport terminalio\nfrom adafruit_matrixportal.matrixportal import MatrixPortal\nimport json\nfrom adafruit_datetime import datetime\nimport supervisor\n\n# Configure one of these examples to create a sign for a stop or station\n# Use only one set of LABEL, DIRECTION, ROUTE, and STOP_ID array variables\n# Each comma separated and quoted entry set of\n# LABEL, DIRECTION, ROUTE, and STOP_ID defines a SIGN\n# ARRAY_VARIABLE = [\"Sign0\",\"Sign1\",\"Sign2\",\"Etc\"]\n\n# Makes three signs for routes at two different stops with scrolling labels for each route sign\n# The sign changes to a new bus route when CYCLE_SIGN is True\n# LABEL = [\"Next 108 bus to Malden Center \",\"Next 411 bus to Malden Center  \",\"Next 430 bus to Saugus Center  \"]\n# DIRECTION = [\"1\",\"1\",\"0\"]         # Stop ID directions of travel\n# ROUTE = [\"108\",\"411\",\"430\"]       # Train or bus routes for your sign\n# STOP_ID = [\"9021\",\"9021\",\"9063\"]  # Stop ID(s) from the MBTA https://api-v3.mbta.com/stops\n\n# Makes two signs for the Malden center subway station showing arriving trains in both directions\n# The scrolling sign changes direction while CYCLE_SIGN is True\n# LABEL = [\"Forest Hills  \",\"Oak Grove  \"]  # Direction of travel labels\n# DIRECTION = [\"0\",\"1\"]                     # Stop direction of travel\n# ROUTE = [\"Orange\",\"Orange\"]               # Train line for your sign\n# STOP_ID = [\"place-mlmnl\",\"place-mlmnl\"]   # Stop ID(s) from the MBTA\n\n# Makes a single sign for one or more routes at a stop\n# Set CYCLE_SIGN to false so that the scroll won't reset and restart\n# The scroll will pause during sign updates, but not restart\nLABEL = [\"Next bus to Malden Center  \"]  # Stop scroll, set CYCLE_SIGN to False when there's only one\nDIRECTION = [\"1\"]                        # Stop direction of travel\nROUTE = [\"108,411,430\"]                  # Bus route(s) for your sign\nSTOP_ID = [\"9021\"]                       # Stop ID(s) from the MBTA\n\n# Pick an image that's appropriate for your sign\nBACKGROUND_IMAGE = \"/t_bus.bmp\"  # t_bus, t_blu, t_orn, t_red, t_grn, t_crl, or t_slv\nCYCLE_SIGN = \"False\"  # If True, cycle through the defined SIGNs, set to False if you make a single sign\nUPDATE_INTERVAL = 30  # Seconds between sign updates\n\nnetwork = None        # Force a network connect at startup\nlast_time_sync = None # Force a time sync at startup\nsync_interval = 3600  # Reset the time every hour\ncurrent_ride = 0      # Start with ride 0\n\n# Set debug to True to see sign changes in the serial monitor\nmatrixportal = MatrixPortal(status_neopixel=board.NEOPIXEL, debug=False)\n\n# Requires an adafruit AIO account to properly work\n# and a settings.toml file with the proper credentials and timezone\nif network is None:\n    network = (matrixportal.network)\n\nmatrixportal.set_background(BACKGROUND_IMAGE) # Maybe make this part of 30 second updates too?\n\nmatrixportal.add_text(     # Add text for the label line at the top\n    text_font=terminalio.FONT,\n    text_position=(18,6),  # Adjust to center a static label if scrolling is False\n    text_scale=1,\n    text_color=0xFFAC1C,\n    scrolling=True,        # Scrolls the sign label, can also be False if your label fits on the sign\n)\nmatrixportal.add_text(     # Add text for the first ride time\n    text_font=terminalio.FONT,\n    text_position=(22, 16),\n    text_scale=1,\n)\nmatrixportal.add_text(     # Add text for the second ride time\n    text_font=terminalio.FONT,\n    text_position=(22, 26),\n    text_scale=1,\n    text=(\"Loading\")\n)\n\ndef get_arrival_in_minutes_from_now(now, date_str):\n    # print(f\"now: {now}, date_str: {date_str}\")  # Uncomment to troubleshoot date and time setting problems\n    ride_date = datetime.fromisoformat(date_str).replace(tzinfo=None)\n    return round((ride_date - now).total_seconds() / 60.0)\n\ndef get_next_ride_times(current_ride):\n    times = []\n    now = datetime.now()\n    try:\n        # Add a page limit to the line below if you get large fetch results from the API to keep your sign from having memory problems\n        DATA_SOURCE = f\"https://api-v3.mbta.com/predictions?filter[stop]={STOP_ID[current_ride]}\u0026filter[route]={ROUTE[current_ride]}\u0026sort=departure_time\"# \u0026page[limit]=3\"\n        response = network.fetch_data(DATA_SOURCE + f\"\u0026filter[direction_id]={DIRECTION[current_ride]}\")\n        # print(DATA_SOURCE + f\"\u0026filter[direction_id]={DIRECTION[current_ride]}\")  # Uncomment to troubleshoot API format problems\n        data = json.loads(response)\n        num_avail_times = len(data[\"data\"])\n        if num_avail_times \u003c 1:\n            return []\n        for i in range(len(data[\"data\"])):\n            try:\n                departure_time = data[\"data\"][i][\"attributes\"][\"departure_time\"]\n                if departure_time:\n                    minute_difference = get_arrival_in_minutes_from_now(now, departure_time)\n                    if (minute_difference \u003e 0):  # Only add positive times to avoid erroneous old values (sometimes the api returns these)\n                        times.append(minute_difference)\n                        if len(times) == 3 or len(times) \u003e= num_avail_times:\n                            break\n            except (KeyError, IndexError):\n                # TODO maybe do more error handling here for bad fetch\n                # Right now just skips to the next departure_time\n                continue\n    except Exception as e:\n        print(f\"Error fetching or parsing data: {e}\")\n        supervisor.reload()\n    return times  # Return the calculated time(s) in an array variable with one or two entries\n\nlast_update = time.monotonic() - UPDATE_INTERVAL\nmatrixportal.set_text(f\"{LABEL[current_ride]}\", 0)  # Set the initial sign scroll\n\nwhile True:\n    if time.monotonic() - last_update \u003e= UPDATE_INTERVAL:\n        if last_time_sync is None or time.monotonic() - last_time_sync \u003e= sync_interval:\n            try:\n                # Set the device time from the network\n                # Be sure to include\n                # timezone = \"US/Eastern\"\n                # in settings.toml or the system time may be set to GMT (UTC? Zulu?)\n                network.get_local_time()\n                last_time_sync = time.monotonic()\n            except Exception as e:\n                print(\"Failed to get the current time, error:\", e)\n                supervisor.reload()\n        now = datetime.now()\n        ride_times = get_next_ride_times(current_ride)\n        matrixportal.set_text(f\"{ride_times[0]:2d} min\" if len(ride_times) \u003e 0 else \"None\", 1)  # Update the first ride time\n        matrixportal.set_text(f\"{ride_times[1]:2d} min\" if len(ride_times) \u003e 1 else \"\", 2)      # Update the second ride time\n        last_update = time.monotonic()\n        if CYCLE_SIGN == \"True\":\n            matrixportal.set_text(f\"{LABEL[current_ride]}\", 0)  # Update the sign scroll\n        if current_ride \u003c (len(ROUTE)-1):\n            current_ride += 1\n        else:\n            current_ride = 0\n        continue\n    matrixportal.scroll()\n    time.sleep(0.05)  # Change the scroll speed here, larger numbers are slower","metadata":{"language":"python","linenums":false}},{"element_type":"product","content":"https://www.adafruit.com/product/4745","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/750/original/bus_sign.jpg?1744046474","metadata":{}},{"element_type":"text","content":"\n        \u003cp\u003eAn LED matrix device to show predictions of arriving buses and trains using the MBTA v3 API\u003c/p\u003e\n\u003cp\u003eTo get to the Git hub repository for this project click here: \u003ca title=\"MBTA Sign\" href=\"https://github.com/dufus2506/MBTA-bus-train-stop-prediction-sign\"\u003ehttps://github.com/dufus2506/MBTA-bus-train-stop-prediction-sign\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eBuilt on the excellent work of others I've seen, I have designed my own MBTA sign to tell you when the bus or train will be leaving a stop. This sign can show predictions for for bus lines, subway stations, and commuter rail stations.\u003c/p\u003e\n\u003cp\u003eThis is designed to be used on the Adafruit Matrixportal M4 running Circuitpython\u003cspan\u003e\u0026nbsp;\u003c/span\u003e\u003ca href=\"https://learn.adafruit.com/adafruit-matrixportal-m4\"\u003ehttps://learn.adafruit.com/adafruit-matrixportal-m4\u003c/a\u003e\u003cspan\u003e\u0026nbsp;\u003c/span\u003eand using a 64x32 RGB LED matrix.\u003cspan\u003e\u0026nbsp;\u003c/span\u003e\u003ca href=\"https://www.adafruit.com/category/327\"\u003ehttps://www.adafruit.com/category/327\u003c/a\u003e\u003cspan\u003e\u0026nbsp;\u003c/span\u003ePick whatever size you like.\u003c/p\u003e\n\u003cp\u003eI managed to keep the code very small, and with a minimum number of libraries so the limited memory of the M4 would not be a problem. And I improved on the code comments a bit to make it easier for you to configure your own sign.\u003c/p\u003e\n\u003cp\u003eI removed all but the most essential libraries from the board. I have only the libraries shown in the image on my board.\u003c/p\u003e\n      ","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/753/original/Lib_listing.png?1744049090","metadata":{}},{"element_type":"text","content":"\n  \n  \n  \n  \n        \u003cp\u003eApart from that I tried to make the sign as useful as possible with the ability to see predictions for more than one stop or bus route. You can add as many stops/routes/lines to a sign as you need, though it's not very practical if the sign has too many. You don't want to be waiting too long for sign updates to see when your ride is leaving.\u003c/p\u003e\n\u003cp\u003eBecause the sign updates every thirty seconds, you won't need an MBTA API Key to make it work. You need one if your project will be checking the API multiple times a second. That's not very practical for this sign, so no API key necessary here. But you will need an Adafruit AIO account so that you can set the clock properly. The free level is just fine for clock setting. Get one here:\u003cspan\u003e\u0026nbsp;\u003c/span\u003e\u003ca href=\"https://io.adafruit.com/\"\u003ehttps://io.adafruit.com/\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003eYour settings.toml file should have the following entries:\u003c/p\u003e\n\u003cp\u003eCIRCUITPY_WIFI_SSID = \"Your_SSID\"\u003c/p\u003e\n\u003cp\u003eCIRCUITPY_WIFI_PASSWORD = \"Your_Password\"\u003c/p\u003e\n\u003cp\u003eAIO_USERNAME = \"Your_AIO_Username\"\u003c/p\u003e\n\u003cp\u003eAIO_KEY = \"aio_Your_Really_Long_AIO_User_Key\"\u003c/p\u003e\n\u003cp\u003etimezone = \"US/Eastern\"\u003c/p\u003e\n\u003cp\u003eDon't forget the timezone entry, or your clock may be set to GMT. WiFi settings are case sensitive in the settings.toml file, so be sure to match everything correctly for your WiFi network.\u003c/p\u003e\n\u003cp\u003eWhen I was done I had the files and directories seen in the image below on the board.\u0026nbsp; All but the lib directory are empty.\u003c/p\u003e\n      \n\n\n\n","metadata":{}},{"element_type":"user_image","content":"https://cdn-learn.adafruit.com/user_assets/assets/000/001/754/original/usb_code.png?1744049153","metadata":{}},{"element_type":"text","content":"\n  \n  \n        \u003cp\u003eSee the code.py file for configuration examples. You only want one set of array variables for your configuration. You'll need some stop information to make the correct entries in your array variables. You can find the necessary settings here:\u003cspan\u003e\u0026nbsp;\u003c/span\u003e\u003ca href=\"https://api-v3.mbta.com/stops\"\u003ehttps://api-v3.mbta.com/stops\u003c/a\u003e\u003cspan\u003e\u0026nbsp;\u003c/span\u003eClick the \"pretty print\" box when the page opens, and search for your stop.\u003c/p\u003e\n      \n\n","metadata":{}}]