diff --git a/.gitignore b/.gitignore index 0fa35b9..f7814f2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,10 @@ fp-info-cache #freecad *.FCBak *.stl + + +#vim +*.swp + +#python +__pycache__ diff --git a/README.md b/README.md index 933e8d6..94d74e3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Raspberry Pi Extension board +# Raspberry Pi Extension Board A custom PLC-like system based on the Raspberry Pi. With a custom interface board, a Raspberry Pi is extended with protected I/O, bus systems, analog inputs, and Relay or PWM outputs. All components, including a display, are housed in a custom-designed enclosure. @@ -8,8 +8,32 @@ The project is intended for versatile use in prototyping and project development --- +# TOC +- [Photos](#photos) +- [Repository Content](#repository-content) +- [Features Overview](#features-overview) +- [Designed PCBs](#designed-pcbs) + * [1. Raspberry Pi Interface Board](#1-raspberry-pi-interface-board) + * [2. **Power Supply Board**](#2---power-supply-board--) + * [3. **LED Boards**](#3---led-boards--) +- [Raspberry Pi Usage](#raspberry-pi-usage) + * [Connectivity](#connectivity) + + [LAN Connection (Ethernet)](#lan-connection--ethernet-) + + [WiFi Connection](#wifi-connection) + + [SSH Access](#ssh-access) + + [Remote Desktop (RDP)](#remote-desktop--rdp-) + + [Mount Raspberry Pi Filesystem in Windows (SAMBA)](#mount-raspberry-pi-filesystem-in-windows--samba-) +- [Python Scripting](#python-scripting) + * [Running Python Examples](#running-python-examples) + * [Starting a New Python Project with I/O Access](#starting-a-new-python-project-with-i-o-access) + * [GUI Interface](#gui-interface) +- [Housing](#housing) +- [Dropped Features](#dropped-features) -### Photo of all hardware components + + +# Photos +### Photo of all Hardware Components

All Components (Disassembled)

@@ -21,7 +45,7 @@ The project is intended for versatile use in prototyping and project development # Repository Content - **KiCad Projects**: 3 KiCad projects with schematics and PCB layouts for the custom PCBs created. - **Housing**: Custom enclosure design for PCBs and Raspberry Pi created in FreeCAD. -- **Software/Firmware**: Python scripts for operating and testing the PCB features (WIP). +- **Software/Firmware**: Python scripts run on the Raspberry Pi for operating and testing the PCB features and providing examples as base for future projects. --- @@ -42,7 +66,7 @@ Detailed features of each PCB are described in the respective sections below. # Designed PCBs -## 1. **Raspberry Pi Interface Board** +## 1. Raspberry Pi Interface Board This board connects to the Raspberry Pi via a 40-pin ribbon cable and provides protected GPIO extensions and versatile input/output features. ### Photo @@ -51,7 +75,7 @@ This board connects to the Raspberry Pi via a 40-pin ribbon cable and provides p ### Features **Inputs**: - **8x Digital Inputs**: - - Wide continuous voltage range (-1.7 V to 120 V), idea: compatible with 3 V and 24 V devices. + - Wide voltage range (-1.7 V to 120 V) → compatible with 3 V and 24 V devices. - TVS diodes for ESD and spike protection. - Reverse polarity protection. - Isolated with optocouplers. @@ -74,7 +98,7 @@ This board connects to the Raspberry Pi via a 40-pin ribbon cable and provides p - 8x digital outputs (via shift register): - Low-power (30 mA push-pull) and high-power (500 mA open-drain) outputs. - Buzzer and relays connected to channels 6-8, with enable/disable switches. - - Note: Outputs are **not short-circuit proof**. + - **Note**: Outputs are **not short-circuit proof**. **General**: - WAGO spring-loaded terminals for easy wiring. @@ -85,7 +109,7 @@ This board connects to the Raspberry Pi via a 40-pin ribbon cable and provides p - UART (unprotected). - I2C (TVS diodes, 2.2 kΩ pull-ups). - SPI (unprotected). -Note: Either RS485 or UART can be used at the same time (select with jumpers). + - **Note**: RS485 and UART cannot be used simultaneously (select via jumpers). ### Schematic and Layout

@@ -160,29 +184,190 @@ Small PCBs with LEDs, resistors, and mounting holes for housing indicators.

---- - - -# Software/Firmware -Python code for operating the extension PCBs (e.g., GPIO pins, shift registers, ADC, and bus communication). This section is a work in progress (WIP), and example scripts will be added as development continues. - +
--- +
+ +# Raspberry Pi Usage + +## Connectivity +The Raspberry Pi can operate **standalone** with a connected keyboard, mouse, and HDMI monitor, or by using the integrated display in the housing. + +However, for **development**, it is recommended to connect it to a network via **WiFi or LAN**, allowing easy remote access from a laptop. A direct **Ethernet connection** between the Raspberry Pi and a laptop is often the simplest method. + +### LAN Connection (Ethernet) +- **Connection**: Connect cable directly between raspberry and PC +- **Configure the PC interface**: Set a static ip address e.g. `192.168.1.1/24` + Note: Optionally also enable network sharing to enable the RPI to use the internet connection the PC currently has. +- **Configure the Raspberry Interface**: Set a static ip address e.g. `192.168.1.100/24` + The Raspberry Pi can be configured for **Ethernet access** using different methods: + - **Boot Partition Configuration:** Modify `cmdline.txt` on the SD card. + - **GUI Setup:** Use the built-in network manager if a monitor is connected. + - **Command Line (`nmcli`)**: Configure networking via the terminal. + +To check the assigned IP run: +```bash +ip a +``` + +### WiFi Connection +Probably the **fastest and easiest** way to set up communication between the Raspberry Pi and a PC - while even maintaining internet access - is to **use a mobile phone as a WiFi hotspot**. +Simply **connect both the Raspberry Pi and the laptop** to the same hotspot. + +To connect to a WiFi network, use: +```bash +nmcli device wifi connect password +``` +Replace `` and `` with the actual credentials. +The Raspberry Pi will **automatically reconnect** after a reboot. + +Once connected, you can check the assigned IP on the Raspberry Pi using: + ``` + ip a + ``` + +### SSH Access +For remote terminal access, connect via SSH from a laptop: +```bash +ssh pi@192.168.1.100 +``` +Replace the IP with the actual address of the Raspberry Pi. + +### Remote Desktop (RDP) +To control the Raspberry Pi’s **GUI remotely**, RDP is pre-configured. + +#### Connect via Windows Remote Desktop: +- Open **Remote Desktop Connection (mstsc)**. +- Enter the Raspberry Pi’s IP address. +- **Login:** + - **User:** `root` + - **Password:** (configured during setup) + - **Note:** Logging in as `pi` may result in a black screen. + +#### Alternatively, use PowerShell: +```powershell +mstsc /v:192.168.1.100:3389 +``` + +### Mount Raspberry Pi Filesystem in Windows (SAMBA) +For convenient file access and editing (e.g., with **VS Code**), the Raspberry Pi’s `/home/pi` directory can be **mounted as a network drive** using Samba. + +#### Steps: +1. Determine the Raspberry Pi’s IP address: + ```bash + ip a + ``` +2. Mount the `/home/pi` directory in Windows: + ```powershell + net use X: \\192.168.1.100\pi /user:pi + ``` + Replace `192.168.1.100` with the actual IP address. + +**Note:** +Currently, only `/home/pi` is available for mounting. To share additional directories, edit the Samba configuration file: +```bash +# add a new `[share]` section with the desired folder path and permissions. +sudo nano /etc/samba/smb.conf +# then restart samba +sudo systemctl restart smbd +``` + + +
+ +--- + +
+ + +# Python Scripting +All **terminal-to-pin assignments** are mapped in [`interface_board_pins.py`](rpi-scripts/interface_board_pins.py), allowing easy reference to terminal numbers labeled on the housing. + +Example scripts demonstrating I/O control can be found in [`rpi-scripts/examples`](rpi-scripts/examples). + +--- + +## Running Python Examples +Pre-written **Python scripts** to control I/O terminals are located in: +``` +rpi-scripts/examples +``` +To run an example, execute: +```bash +cd /home/pi/git/rpi-interface-board/rpi-scripts/examples/ +python read_digital_inputs.py +``` + +--- + +## Starting a New Python Project with I/O Access +To create a custom Python project using the **interface board**, follow these steps: + +1. **Copy the entire `rpi-scripts/` folder** to your new project directory. +2. **Remove unnecessary files**, keeping at least: + - `rpi-scripts/interface_board_libs/` → contains custom drivers. + - `rpi-scripts/interface_board_pins.py` → maps terminal numbers to GPIO/ADC channels. +3. **Start coding** by modifying an existing example in `examples/`. + +--- + +## GUI Interface +A **Python GUI** is available for **real-time monitoring** and **control** of all I/O terminals for quick testing. +Currently it auto starts after boot and shows on the integrated display in fullscreen mode. + +### Run the GUI +```bash +cd rpi-scripts/gui +python main.py +``` + +### Enable GUI Autostart on Boot +The GUI is configured to start automatically on boot in **fullscreen mode**. To modify this behavior: + +#### Enable autostart: +```bash +sudo cp rpi-scripts/gui/gui-start.service /etc/systemd/system/ +sudo systemctl enable gui-start.service +``` + +#### Disable autostart: +```bash +sudo systemctl disable gui-start.service +``` + +### GUI Screenshots +

+ + + +

+ + +
+ +--- + +
+ # Housing Custom-designed enclosure includes: -- Ports for Raspberry Pi. -- Banana sockets for power outputs. -- Exposed screw terminals. -- Openings for buttons and LEDs. -- Mounts for all PCBs and wiring. - +- Cutouts for all Ports for Raspberry Pi. +- Mounting screws for all pcbs and Raspberry +- Fan mount + venting slots +- Cutouts for all external pcb terminals +- Cutout + mounting arms (M2.5) for Display 3D Model +
+ --- +
+ # Dropped Features The following ideas were considered but not implemented: diff --git a/doc/graphics/GUI_tab-adc-plot.png b/doc/graphics/GUI_tab-adc-plot.png new file mode 100644 index 0000000..e0f9b6a Binary files /dev/null and b/doc/graphics/GUI_tab-adc-plot.png differ diff --git a/doc/graphics/GUI_tab-control.png b/doc/graphics/GUI_tab-control.png new file mode 100644 index 0000000..c192d55 Binary files /dev/null and b/doc/graphics/GUI_tab-control.png differ diff --git a/doc/graphics/GUI_tab-digital-plot.png b/doc/graphics/GUI_tab-digital-plot.png new file mode 100644 index 0000000..76ccc9f Binary files /dev/null and b/doc/graphics/GUI_tab-digital-plot.png differ diff --git a/rpi-scripts/examples/pass_through_inputs_to_outputs.py b/rpi-scripts/examples/pass_through_inputs_to_outputs.py new file mode 100644 index 0000000..f42b6fb --- /dev/null +++ b/rpi-scripts/examples/pass_through_inputs_to_outputs.py @@ -0,0 +1,64 @@ +import os +import sys +import time +import RPi.GPIO as GPIO + + + +# Import pin assignments and custom libraries +# Add the parent directory to the module search path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from interface_board_pins import GPIO_DIGITAL_INPUTS # Input GPIO mapping +from interface_board_pins import ( # Shift register pin assignment + GPIO_SHIFT_REG_DATA, + GPIO_SHIFT_REG_LATCH, + GPIO_SHIFT_REG_CLOCK, +) +from interface_board_libs.shift_register import ShiftRegister # Custom shift register class + + + +# Initialize GPIO for digital inputs +GPIO.setmode(GPIO.BCM) +for pin in GPIO_DIGITAL_INPUTS.values(): + print(f"Configuring GPIO pin {pin} as input") + GPIO.setup(pin, GPIO.IN) + + + +# Initialize shift register +sr = ShiftRegister(GPIO_SHIFT_REG_DATA, GPIO_SHIFT_REG_LATCH, GPIO_SHIFT_REG_CLOCK) + + + +# Main loop: Read inputs and write to shift register +print("\nStarting passthrough mode: Digital inputs → Shift register outputs") +try: + while True: + shift_register_value = 0 # Byte to be written to the shift register + print("\nReading inputs and updating shift register...") + + for i, (label, pin) in enumerate(GPIO_DIGITAL_INPUTS.items()): + state = GPIO.input(pin) # Read GPIO state + print(f"Input {label} (GPIO {pin}): {'HIGH' if state else 'LOW'}") + + # Set corresponding bit in shift register byte + if state: + shift_register_value |= (1 << i) # Set bit at position `i` + else: + shift_register_value &= ~(1 << i) # Clear bit at position `i` + + # Write the final byte to the shift register + sr.write_byte(shift_register_value) + print(f"Shift Register Output: {format(shift_register_value, '08b')[::-1]}") # Print binary representation (mirrored, lsb first = terminal order) + + time.sleep(0.3) # Small delay to prevent excessive polling + +except KeyboardInterrupt: + print("\nExiting...") + +finally: + sr.clear() # Clear shift register before exiting + print("Shift register cleared.") + GPIO.cleanup() # Cleanup GPIO settings + diff --git a/rpi-scripts/examples/read_analog_inputs.py b/rpi-scripts/examples/read_analog_inputs.py new file mode 100644 index 0000000..ad54907 --- /dev/null +++ b/rpi-scripts/examples/read_analog_inputs.py @@ -0,0 +1,79 @@ +# Include external libraries +import os +import sys +import time + + +# Include custom files +# Add the parent directory to the module search path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from interface_board_libs.adc_mcp3208 import MCP3208 # custom driver for ADC-IC +from interface_board_pins import ( + # mapping of ADC channels to terminals + max value: + ADC_TERMINALS, + # mapping of ADC channels to terminals: + ADC_CHANNEL_T0__0_TO_3V3, + ADC_CHANNEL_T1__0_TO_3V3, + ADC_CHANNEL_T2__0_TO_5V, + ADC_CHANNEL_T3__0_TO_5V, + ADC_CHANNEL_T4__0_TO_12V, + ADC_CHANNEL_T5__0_TO_24V, + ADC_CHANNEL_T6__0_TO_20MA, + ADC_CHANNEL_T7__0_TO_20MA, +) # mapping of ADC channels to terminals + + +# create ADC instance +adc = MCP3208() + + + +# local helper function to scale the adc value to an actual voltage +def adc2value(adc_value, max_value): + max_adc = 4095 + return adc_value * max_value / max_adc + + + + +while True: + print("-" * 40) + + # Read all available channels in a loop according to terminal order / map + values = [] + for terminal, config in ADC_TERMINALS.items(): + adc_channel = config["adc_channel"] + max_value = config["max_value"] + value = adc.read(adc_channel) + print(f"T{terminal}: ADC={value:04d} => U_ADC = {adc2value(value, max_value):5.3f}V") + + + # Read channels one by one using defined constants (more intuitive) + print("-" * 40) + value = adc.read(ADC_CHANNEL_T0__0_TO_3V3) + print(f"Terminal 0 (0 to 3.3V): ADC={value:04d} => Terminal={adc2value(value, 3.3):4.2f}V") + + value = adc.read(ADC_CHANNEL_T1__0_TO_3V3) + print(f"Terminal 1 (0 to 3.3V): ADC={value:04d} => Terminal={adc2value(value, 3.3):4.2f}V") + + value = adc.read(ADC_CHANNEL_T2__0_TO_5V) + print(f"Terminal 2 (0 to 5V): ADC={value:04d} => Terminal={adc2value(value, 5):4.2f}V") + + value = adc.read(ADC_CHANNEL_T3__0_TO_5V) + print(f"Terminal 3 (0 to 5V): ADC={value:04d} => Terminal={adc2value(value, 5):4.2f}V") + + value = adc.read(ADC_CHANNEL_T4__0_TO_12V) + print(f"Terminal 4 (0 to 12V): ADC={value:04d} => Terminal={adc2value(value, 12):05.2f}V") + + value = adc.read(ADC_CHANNEL_T5__0_TO_24V) + print(f"Terminal 5 (0 to 24V): ADC={value:04d} => Terminal={adc2value(value, 24):05.2f}V") + + value = adc.read(ADC_CHANNEL_T6__0_TO_20MA) + print(f"Terminal 6 (0 to 20mA): ADC={value:04d} => Terminal={adc2value(value, 20):05.2f}mA") + + value = adc.read(ADC_CHANNEL_T7__0_TO_20MA) + print(f"Terminal 7 (0 to 20mA): ADC={value:04d} => Terminal={adc2value(value, 20):05.2f}mA") + + print("-" * 40) + time.sleep(1) # Delay before next read cycle + diff --git a/rpi-scripts/examples/read_digital_inputs.py b/rpi-scripts/examples/read_digital_inputs.py new file mode 100644 index 0000000..3a3dde7 --- /dev/null +++ b/rpi-scripts/examples/read_digital_inputs.py @@ -0,0 +1,37 @@ +import os +import sys +# Add the parent directory to the module search path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +# Include the common pin assignment (from parent folder) +from interface_board_pins import GPIO_DIGITAL_INPUTS # map of which GPIO pins are associated with which input terminal (1-8) + +import RPi.GPIO as GPIO +import time + + + +# Initialize GPIO +GPIO.setmode(GPIO.BCM) +for pin in GPIO_DIGITAL_INPUTS.values(): + print(f"configuring pin {pin} as input") + GPIO.setup(pin, GPIO.IN) + + + +# Repeatedly read GPIOs +print("Reading digital inputs:") +try: + while True: + print("reading all gpio pins...") + for label, pin in GPIO_DIGITAL_INPUTS.items(): + print(f"reading pin {pin}") + state = GPIO.input(pin) + print(f"Input {label} (GPIO {pin}): {'HIGH' if state else 'LOW'}") + print("-" * 40) + time.sleep(0.5) + +except KeyboardInterrupt: + print("Exiting...") + +finally: + GPIO.cleanup() diff --git a/rpi-scripts/examples/write_digital_outputs_shift_reg.py b/rpi-scripts/examples/write_digital_outputs_shift_reg.py new file mode 100644 index 0000000..7b67839 --- /dev/null +++ b/rpi-scripts/examples/write_digital_outputs_shift_reg.py @@ -0,0 +1,82 @@ +# Include external libraries +import os +import sys +import RPi.GPIO as GPIO +from time import sleep + + +# Include custom files +# Add the parent directory to the module search path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from interface_board_libs.shift_register import ShiftRegister # custom shift register class +from interface_board_pins import ( # pin / channel assignment + GPIO_SHIFT_REG_DATA, + GPIO_SHIFT_REG_LATCH, + GPIO_SHIFT_REG_CLOCK, + SHIFT_REG_CHANNEL_BUZZER, + SHIFT_REG_CHANNEL_RELAY1, + SHIFT_REG_CHANNEL_RELAY2, +) + + + +# Config +COUNT_UP_TEST_ENABLED = False +DELAY_COUNT_UP = 0.008 +DELAY_TOGGLE = 0.3 + + +# Initialize the shift register +sr = ShiftRegister(GPIO_SHIFT_REG_DATA, GPIO_SHIFT_REG_LATCH, GPIO_SHIFT_REG_CLOCK) + +try: + print("Writing to shift register...") + + + # repeatedly write to shift register + while True: + # Cycle through all combinations of pin states (write entire byte) + if COUNT_UP_TEST_ENABLED: + print("Writing all byte values (0-255)...") + for value in range(256): + sr.write_byte(value) # Write the current value + print(f"Output: {'{0:08b}'.format(value)}") # Print binary representation + sleep(DELAY_COUNT_UP) # Delay between each byte + + sleep(0.5) + sr.clear() # Clear the shift register + + + # Turn each pin on and off one by one + print("\nToggling each pin one by one...") + # channels 0-2 are connected to buzzer and relays: + print(f"Num {SHIFT_REG_CHANNEL_BUZZER}: Toggling buzzer...") # channel 0 + sr.set_pin(SHIFT_REG_CHANNEL_BUZZER, True) # Turn buzzer ON + sleep(DELAY_TOGGLE) + sr.set_pin(SHIFT_REG_CHANNEL_BUZZER, False) # Turn buzzer OFF + + print(f"Num {SHIFT_REG_CHANNEL_RELAY1}: Toggling Relay 1...") # channel 1 + sr.toggle_pin(SHIFT_REG_CHANNEL_RELAY1) + sleep(DELAY_TOGGLE) + sr.toggle_pin(SHIFT_REG_CHANNEL_RELAY1) + + print(f"Num {SHIFT_REG_CHANNEL_RELAY2}: Toggling Relay 2...") # channel 2 + sr.set_pin(SHIFT_REG_CHANNEL_RELAY2, True) # Turn relay ON + sleep(DELAY_TOGGLE) + sr.set_pin(SHIFT_REG_CHANNEL_RELAY2, False) # Turn relay OFF + + + # channels 3-7 are connected to terminals only (no dedicated device on pcb): + for pin in range(3,8): # 3-7 are connected to terminal only + print(f"Num: {pin}: Toggling Terminal") + sr.set_pin(pin, True) # Set the pin HIGH + sleep(DELAY_TOGGLE) + #print(f"Setting pin {pin} LOW.") + sr.set_pin(pin, False) # Set the pin LOW + #sleep(0.5) + + +finally: + sr.clear() # Clear the shift register + print("Shift register cleared.") + GPIO.cleanup() # Clean up GPIO settings diff --git a/rpi-scripts/examples/write_pwm_outputs.py b/rpi-scripts/examples/write_pwm_outputs.py new file mode 100644 index 0000000..f85aaed --- /dev/null +++ b/rpi-scripts/examples/write_pwm_outputs.py @@ -0,0 +1,76 @@ +# Include external libraries +import os +import sys +import RPi.GPIO as GPIO +from time import sleep + + +# Include custom files +# Add the parent directory to the module search path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from interface_board_pins import ( # pin / channel assignment + GPIO_PWM1, # RPI_PWM0 + GPIO_PWM2 # RPI_PWM0 too +) + + + +# Config +BLINK_ONLY = False + + + +# PWM Settings +FREQ = 1000 # PWM frequency in Hz +STEP = 2 # Step size for fading +DELAY = 0.03 # Delay between steps + +try: + print("Configuring PWM pins...") + GPIO.setmode(GPIO.BCM) + GPIO.setup(GPIO_PWM1, GPIO.OUT) + GPIO.setup(GPIO_PWM2, GPIO.OUT) + + + if BLINK_ONLY: + while True: + print(f"PWM1 ON (GPIO {GPIO_PWM1})") + GPIO.output(GPIO_PWM1, 1) + sleep(2) + print("PWM1 OFF") + GPIO.output(GPIO_PWM1, 0) + print(f"PWM2 ON (GPIO {GPIO_PWM2})") + GPIO.output(GPIO_PWM2, 1) + sleep(2) + print("PWM2 OFF") + GPIO.output(GPIO_PWM2, 0) + + + # Initialize PWM on both pins + pwm1 = GPIO.PWM(GPIO_PWM1, FREQ) + pwm2 = GPIO.PWM(GPIO_PWM2, FREQ) + + pwm1.start(0) # Start with 0% duty cycle + pwm2.start(100) # Start with 100% duty cycle + + print("Starting PWM fade effect...") + while True: + # Fade up PWM1 and fade down PWM2 + for duty in range(0, 101, STEP): # Duty cycle from 0% to 100% + pwm1.ChangeDutyCycle(duty) + pwm2.ChangeDutyCycle(100 - duty) # Opposite fade + print(f"PWM1: {duty}% | PWM2: {100 - duty}%") + sleep(DELAY) + + # Fade down PWM1 and fade up PWM2 + for duty in range(100, -1, -STEP): # Duty cycle from 100% to 0% + pwm1.ChangeDutyCycle(duty) + pwm2.ChangeDutyCycle(100 - duty) # Opposite fade + print(f"PWM1: {duty}% | PWM2: {100 - duty}%") + sleep(DELAY) + +finally: + print("Exiting, stopping PWM and cleaning up...") + pwm1.stop() + pwm2.stop() + GPIO.cleanup() # Clean up GPIO settings diff --git a/rpi-scripts/gui/gui-start.service b/rpi-scripts/gui/gui-start.service new file mode 100644 index 0000000..c8ac916 --- /dev/null +++ b/rpi-scripts/gui/gui-start.service @@ -0,0 +1,24 @@ +# systemd service file for auto starting the python gui +# at the correct time after startup (when window manager finished starting) + + +# Usage: +# - Install: `sudo cp rpi-scripts/gui/gui-start.service /etc/systemd/system/` +# - Enable autostart: `sudo systemctl enable gui-start.service` +# - Disable autostart: `sudo systemctl disable gui-start.service` + +[Unit] +Description=Start Python GUI for IO-control and monitoring after startup +After=display-manager.service +Wants=display-manager.service + +[Service] +Environment=DISPLAY=:0 +ExecStart=/usr/bin/python3 /home/pi/git/rpi-interface-board/rpi-scripts/gui/main.py +WorkingDirectory=/home/pi/git/rpi-interface-board/rpi-scripts/gui +User=pi +Group=pi +Restart=always + +[Install] +WantedBy=default.target diff --git a/rpi-scripts/gui/main.py b/rpi-scripts/gui/main.py new file mode 100644 index 0000000..70ac308 --- /dev/null +++ b/rpi-scripts/gui/main.py @@ -0,0 +1,90 @@ +import os +import sys +import time +import tkinter as tk +from tkinter import ttk +from tkinter import messagebox +import RPi.GPIO as GPIO + +# Add the parent directory to the module search path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from interface_board_libs.adc_mcp3208 import MCP3208 +from interface_board_libs.shift_register import ShiftRegister +from interface_board_pins import * # Import pin assignments +from tab_control import create_control_tab +from tab_control import set_updating_enabled as tab_control__set_updating_enabled +from tab_adc_plot import create_adc_plot_tab +from tab_adc_plot import set_updating_enabled as tab_analog_plot__set_updating_enabled +from tab_digital_plot import create_digital_plot_tab +from tab_digital_plot import set_updating_enabled as tab_digital_plot__set_updating_enabled +from tab_exit import create_exit_tab + + +# Initialize ADC & Shift Register +adc = MCP3208() +shift_reg = ShiftRegister(GPIO_SHIFT_REG_DATA, GPIO_SHIFT_REG_LATCH, GPIO_SHIFT_REG_CLOCK) + + +# GPIO Setup +GPIO.setmode(GPIO.BCM) +for pin in GPIO_DIGITAL_INPUTS.values(): + GPIO.setup(pin, GPIO.IN) +GPIO.setup(GPIO_PWM1, GPIO.OUT) +GPIO.setup(GPIO_PWM2, GPIO.OUT) +pwm1 = GPIO.PWM(GPIO_PWM1, 1000) +pwm2 = GPIO.PWM(GPIO_PWM2, 1000) +pwm1.start(0) +pwm2.start(0) + + +# Tkinter GUI +root = tk.Tk() +root.title("Raspberry Pi Interface Board") +root.attributes('-fullscreen', True) +root.configure(bg="black") + + +# Tabbed Interface +notebook = ttk.Notebook(root) +notebook.pack(expand=True, fill="both") + + +# Track active tab +def on_tab_change(event): + active_tab = event.widget.tab(event.widget.index("current"), "text") + print (f"INFO: switched to tab {active_tab}") + match active_tab: + case "ADC Plot": + tab_control__set_updating_enabled(False) + tab_analog_plot__set_updating_enabled(True) + tab_digital_plot__set_updating_enabled(False) + case "Digital Inputs": + tab_control__set_updating_enabled(False) + tab_analog_plot__set_updating_enabled(False) + tab_digital_plot__set_updating_enabled(True) + case "Controls": + tab_control__set_updating_enabled(True) + tab_analog_plot__set_updating_enabled(False) + tab_digital_plot__set_updating_enabled(False) + case "EXIT": + pass + case _: + print(f"unhandled change to tab {active_tab}") + +notebook.bind("<>", on_tab_change) + + +# Add tabs +create_control_tab(notebook, adc, shift_reg, pwm1, pwm2) +create_adc_plot_tab(notebook, adc) +create_digital_plot_tab(notebook) +create_exit_tab(notebook, root, pwm1, pwm2) + + +# Run GUI +try: + root.mainloop() + +except KeyboardInterrupt: + print('Keyboard interrupt -> exiting') + exit() \ No newline at end of file diff --git a/rpi-scripts/gui/tab_adc_plot.py b/rpi-scripts/gui/tab_adc_plot.py new file mode 100644 index 0000000..1ca176b --- /dev/null +++ b/rpi-scripts/gui/tab_adc_plot.py @@ -0,0 +1,86 @@ +import sys +import os +import tkinter as tk +from tkinter import ttk +from matplotlib.figure import Figure +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import time + +# Add the parent directory to the module search path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from interface_board_pins import ADC_TERMINALS # Import pin assignments + + +# CONFIG +ADC_PLOT_UPDATE_DELAY_MS = 50 # note: delay is additional to time it takes to update the chart +TAB_NOT_ACTIVE_CHECK_INTERVAL_MS = 2000 +MAX_X_HISTORY_VALUES = 150 +AUTO_SCALE_ENABLED = True +DEBUG = False + + +# Variables +updating_enabled = False +adc_channels = list(range(8)) +data = {ch: [0] * MAX_X_HISTORY_VALUES for ch in adc_channels} # Preallocate lists +time_data = list(range(-MAX_X_HISTORY_VALUES, 0)) # Simulated time axis + + +def create_adc_plot_tab(notebook, adc): + frame = ttk.Frame(notebook) + notebook.add(frame, text="ADC Plot") + + figure = Figure(figsize=(8, 5), dpi=100) + ax = figure.add_subplot(1, 1, 1) + + ax.set_title("ADC Inputs") # Set title + ax.set_xlabel("Time (s)") # X-axis label + ax.set_ylabel("Voltage / Current (V / mA)") # Y-axis label + + ax.set_xlim(-MAX_X_HISTORY_VALUES, 0) # Keep time axis fixed + canvas = FigureCanvasTkAgg(figure, master=frame) + canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + + # Initialize lines for fast updates + lines = {} + for terminal, config in ADC_TERMINALS.items(): + label = f"T{terminal} ({config['max_value']} {config['unit']})" # Format: T1 (3.3V) + lines[terminal] = ax.plot(time_data, data[terminal], label=label)[0] + + ax.legend() # Show legend + + def update_plot(): + if not updating_enabled: + frame.after(TAB_NOT_ACTIVE_CHECK_INTERVAL_MS, update_plot) + return + + for terminal, config in ADC_TERMINALS.items(): + adc_channel = config["adc_channel"] + max_value = config["max_value"] + # Shift existing data left and + # read new data + scale based on the correct max voltage + data[terminal].pop(0) + data[terminal].append(round(adc.read(adc_channel) * max_value / 4095, 2)) + + # Recalculate limits (autoscale) + if AUTO_SCALE_ENABLED: + ax.set_ylim(bottom=0) # Lower limit always 0 + ax.autoscale(enable=True, axis='y', tight=False) # Enable autoscale for upper limit + ax.relim() # Recalculate limits + ax.autoscale_view(scalex=False, scaley=True) # Autoscale only y-axis + else: + ax.set_ylim(0, 24) # Fixed range if autoscale is disabled + + for terminal in ADC_TERMINALS: + lines[terminal].set_ydata(data[terminal]) + + canvas.draw_idle() # Efficient redraw + frame.after(ADC_PLOT_UPDATE_DELAY_MS, update_plot) + + update_plot() + + +def set_updating_enabled(is_active): + global updating_enabled + updating_enabled = is_active + if DEBUG: print(f"adc_plot tab: set updating_enabled to {updating_enabled}") diff --git a/rpi-scripts/gui/tab_control.py b/rpi-scripts/gui/tab_control.py new file mode 100644 index 0000000..21898d1 --- /dev/null +++ b/rpi-scripts/gui/tab_control.py @@ -0,0 +1,141 @@ +import sys +import os +import tkinter as tk +from tkinter import ttk +import RPi.GPIO as GPIO + +# Add the parent directory to the module search path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from interface_board_pins import * # Import pin assignments + + +# CONFIG +ADC_VALUES_UPDATE_INTERVAL_MS = 500 +DIGITAL_INPUTS_UPDATE_INTERVAL_MS = 200 +TAB_NOT_ACTIVE_CHECK_INTERVAL_MS = 2000 +DEBUG = False + + +# Variables +updating_enabled = False + + +def create_control_tab(notebook, adc, shift_reg, pwm1, pwm2): + frame = ttk.Frame(notebook) + notebook.add(frame, text="Controls") + + digital_input_states = [tk.StringVar(value="LOW") for _ in range(8)] + digital_output_states = [tk.BooleanVar(value=False) for _ in range(8)] + adc_values = [tk.StringVar(value=" --- ") for _ in range(8)] + pwm_values = [tk.IntVar(value=0), tk.IntVar(value=0)] + output_buttons = {} # Store button references to change colors + + + def update_inputs(): + if not updating_enabled: + frame.after(TAB_NOT_ACTIVE_CHECK_INTERVAL_MS, update_inputs) + return + for i, pin in enumerate(GPIO_DIGITAL_INPUTS.values()): + digital_input_states[i].set("HIGH" if GPIO.input(pin) else "LOW") + frame.after(DIGITAL_INPUTS_UPDATE_INTERVAL_MS, update_inputs) + + + def update_adc(): + if not updating_enabled: + frame.after(TAB_NOT_ACTIVE_CHECK_INTERVAL_MS, update_adc) + return + + for terminal, config in ADC_TERMINALS.items(): + adc_channel = config["adc_channel"] + max_value = config["max_value"] + unit = config["unit"] + + # Read ADC value and scale to the correct voltage + value = adc.read(adc_channel) + adc_values[terminal].set(f"{value * max_value / 4095:4.2f} {unit}") + + frame.after(ADC_VALUES_UPDATE_INTERVAL_MS, update_adc) + + + def toggle_output(index): + """ Toggle shift register output and update button color. """ + new_state = not digital_output_states[index].get() + digital_output_states[index].set(new_state) + shift_reg.set_pin(index, new_state) + + if index in output_buttons: + for btn in output_buttons[index]: + btn.configure(bg="green" if new_state else "red", activebackground="green" if new_state else "red") + + + style = ttk.Style() + style.configure("TScale", thickness=60) + style.configure("TButton", font=("Arial", 18), padding=5) + + control_frame = ttk.Frame(frame, padding=40) + control_frame.grid(row=0, column=0, sticky="nsew") + + frame.grid_rowconfigure(0, weight=1) + frame.grid_rowconfigure(1, weight=0) + frame.grid_columnconfigure(0, weight=1) + + for i in range(8): + ttk.Label(control_frame, text=f"ADC-{i}: ", font=("Arial", 18)).grid(row=i, column=0, sticky="e") + ttk.Label(control_frame, textvariable=adc_values[i], width=10, font=("Arial", 18)).grid(row=i, column=1, sticky="w") + ttk.Label(control_frame, text=f"IN-{i}:", font=("Arial", 18)).grid(row=i, column=2, sticky="e", padx=20) + ttk.Label(control_frame, textvariable=digital_input_states[i], width=6, font=("Arial", 18)).grid(row=i, column=3, sticky="w") + + btn = tk.Button(control_frame, text=f"OUT-{i}", font=("Arial", 16), width=10, bg="red", activebackground="red", command=lambda i=i: toggle_output(i)) + btn.grid(row=i, column=4, sticky="w", padx=20) + + output_buttons.setdefault(i, []).append(btn) + + if i == 0: + buzzer_btn = tk.Button(control_frame, text="BUZZER", font=("Arial", 16), width=10, bg="red", activebackground="red", command=lambda: toggle_output(0)) + buzzer_btn.grid(row=i, column=5, padx=20) + output_buttons[0].append(buzzer_btn) + + if i == 1: + relay2_btn = tk.Button(control_frame, text="RELAY2", font=("Arial", 16), width=10, bg="red", activebackground="red", command=lambda: toggle_output(1)) + relay2_btn.grid(row=i, column=5, padx=20) + output_buttons[1].append(relay2_btn) + + if i == 2: + relay1_btn = tk.Button(control_frame, text="RELAY1", font=("Arial", 16), width=10, bg="red", activebackground="red", command=lambda: toggle_output(2)) + relay1_btn.grid(row=i, column=5, padx=20) + output_buttons[2].append(relay1_btn) + + pwm_frame = ttk.Frame(frame, padding=20) + pwm_frame.grid(row=1, column=0, sticky="ew") + + pwm_frame.columnconfigure(0, weight=0) + pwm_frame.columnconfigure(1, weight=1) + pwm_frame.columnconfigure(2, weight=0) + + pwm1_label = ttk.Label(pwm_frame, text="PWM-1:", font=("Arial", 18)) + pwm1_label.grid(row=0, column=0, padx=20, sticky="e") + + pwm1_slider = ttk.Scale( + pwm_frame, from_=0, to=100, orient="horizontal", length=400, + variable=pwm_values[0], command=lambda val: pwm1.ChangeDutyCycle(int(float(val))) + ) + pwm1_slider.grid(row=0, column=1, columnspan=2, sticky="we", padx=10) + + pwm2_label = ttk.Label(pwm_frame, text="PWM-2:", font=("Arial", 18)) + pwm2_label.grid(row=1, column=0, padx=20, sticky="e") + + pwm2_slider = ttk.Scale( + pwm_frame, from_=0, to=100, orient="horizontal", length=400, + variable=pwm_values[1], command=lambda val: pwm2.ChangeDutyCycle(int(float(val))) + ) + pwm2_slider.grid(row=1, column=1, columnspan=2, sticky="we", padx=10) + + update_inputs() + update_adc() + + + +def set_updating_enabled(is_active): + global updating_enabled + updating_enabled = is_active + if DEBUG: print(f"control tab: set updating_enabled to {updating_enabled}") diff --git a/rpi-scripts/gui/tab_digital_plot.py b/rpi-scripts/gui/tab_digital_plot.py new file mode 100644 index 0000000..2d14ca0 --- /dev/null +++ b/rpi-scripts/gui/tab_digital_plot.py @@ -0,0 +1,78 @@ +import sys +import os +import threading +import tkinter as tk +from tkinter import ttk +import RPi.GPIO as GPIO +from matplotlib.figure import Figure +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import time +# Add the parent directory to the module search path +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from interface_board_pins import * # Import pin assignments + + +# CONFIG +DIGITAL_PLOT_UPDATE_DELAY_MS = 50 # note: delay is additional to time it takes to update the chart +TAB_NOT_ACTIVE_CHECK_INTERVAL_MS = 2000 # Check inactive tabs less frequently +MAX_X_HISTORY_VALUES = 100 # Keep last 100 values for scrolling +DEBUG = False + + +# Variables +updating_enabled = False # Track if the tab is active +input_channels = list(range(8)) +data = {ch: [0] * MAX_X_HISTORY_VALUES for ch in input_channels} # Preallocate data +time_data = list(range(-MAX_X_HISTORY_VALUES, 0)) # Simulated time axis + + +def create_digital_plot_tab(notebook): + frame = ttk.Frame(notebook) + notebook.add(frame, text="Digital Inputs") + + figure = Figure(figsize=(8, 5), dpi=100) + ax = figure.add_subplot(1, 1, 1) + + ax.set_title("Digital Inputs") # Set title + ax.set_xlabel("Time (s)") # X-axis label + ax.set_ylabel("State (1=HIGH / 0=LOW)") # Y-axis label + + ax.set_ylim(-0.2, 1.2) + ax.set_xlim(-MAX_X_HISTORY_VALUES, 0) # Keep time axis fixed + canvas = FigureCanvasTkAgg(figure, master=frame) + canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + + # Initialize lines for fast updates + lines = {} + for ch in input_channels: + label = f"Digital In {ch}" + lines[ch] = ax.step(time_data, data[ch], where="post", label=label)[0] + + ax.legend() # Show legend + + def update_plot(): + if not updating_enabled: + frame.after(TAB_NOT_ACTIVE_CHECK_INTERVAL_MS, update_plot) + return + + # Shift existing data left + for ch in input_channels: + data[ch].pop(0) + data[ch].append(GPIO.input(GPIO_DIGITAL_INPUTS[ch])) + + # Update only the y-data for efficiency + for ch in input_channels: + lines[ch].set_ydata(data[ch]) + + canvas.draw_idle() # More efficient than draw() + frame.after(DIGITAL_PLOT_UPDATE_DELAY_MS, update_plot) + + update_plot() + + + +def set_updating_enabled(is_active): + global updating_enabled + updating_enabled = is_active + if DEBUG: print(f"digital_plot tab: set updating_enabled to {updating_enabled}") + diff --git a/rpi-scripts/gui/tab_exit.py b/rpi-scripts/gui/tab_exit.py new file mode 100644 index 0000000..81f6edb --- /dev/null +++ b/rpi-scripts/gui/tab_exit.py @@ -0,0 +1,21 @@ + +import tkinter as tk +from tkinter import ttk + +def create_exit_tab(notebook, root, pwm1, pwm2): + frame = ttk.Frame(notebook) + notebook.add(frame, text="EXIT") + + ttk.Label(frame, text="Exit the GUI", font=("Arial", 20)).pack(pady=20) + + def exit_program(): + pwm1.stop() + pwm2.stop() + root.quit() + root.destroy() + + exit_button = ttk.Button(frame, text="Exit Application", command=exit_program, style="TButton") + exit_button.pack(pady=20) + + style = ttk.Style() + style.configure("TButton", font=("Arial", 18), padding=10) diff --git a/rpi-scripts/interface_board_libs/adc_mcp3208.py b/rpi-scripts/interface_board_libs/adc_mcp3208.py new file mode 100644 index 0000000..d985ecf --- /dev/null +++ b/rpi-scripts/interface_board_libs/adc_mcp3208.py @@ -0,0 +1,74 @@ +import spidev +import time + +DEBUG = False + + +class MCP3208: + def __init__(self, bus=0, device=0): + # Initialize SPI bus and device + self.spi = spidev.SpiDev() + self.spi.open(bus, device) # Default SPI bus 0, device 0 (you can change as needed) + self.spi.max_speed_hz = 1000000 # Adjust based on your needs (e.g., 1MHz) + self.spi.mode = 0b00 # Set SPI mode (Mode 0 for MCP3208) + + + def read(self, channel): + """ + Read the ADC value from the specified channel (0-7). + """ + + # MCP3208 is a 12-bit SPI ADC. Communication requires sending a 3-byte command and receiving a 3-byte response. + # The bits in the command sequence are structured as follows: + + # Command Byte (8 bits): + # | Start (1) | Mode (1) | Channel (3) | Padding (3) | + + # The MCP3208 responds with 3 bytes: + # - Byte 1: Contains the highest bit (bit 11) of the 12-bit ADC result. + # - Byte 2: Contains bits 10-3 of the ADC result. + # - Byte 3: Contains bits 2-0 of the ADC result. + + # Ensure the channel is valid (0-7) + if channel < 0 or channel > 7: + raise ValueError(f"Channel must be between 0 and 7. (current channel={channel})") + + # Construct the command byte sequence: + # - Start bit (1) -> 1000 0000 (0x80) + # - Single-ended (1) -> 1100 0000 (0xC0) + # - Channel (3 bits) shifted into position + cmd = 0x80 # Start bit: 1000 0000 + cmd |= 0x40 # Single-ended mode: 1100 0000 + cmd |= (channel & 0x07) << 3 # Move channel number to bits 5-3 + + # Send the command and receive the 3-byte response + ret = self.spi.xfer2([cmd, 0x00, 0x00]) # Send 3 bytes, receive 3 bytes + + if DEBUG: + print(f"result[0]: {bin(ret[0])}, result[1]: {bin(ret[1])}, result[2]: {bin(ret[2])}") + + # Extract the 12-bit ADC result from the received bytes: + val = (ret[0] & 0x01) << 11 # Extract bit 11 (MSB) + val |= ret[1] << 3 # Extract bits 10-3 + val |= ret[2] >> 5 # Extract bits 2-0 (shift down) + + return val & 0x0FFF # Mask to ensure only the lower 12 bits are used + + + def close(self): + """Close SPI connection.""" + self.spi.close() + + + +# Example usage: +if __name__ == "__main__": + adc = MCP3208() + try: + while True: + for i in range(8): + adc_value = adc.read(i) + print(f'ADC[{i}]: {adc_value}') + time.sleep(0.5) + finally: + adc.close() diff --git a/rpi-scripts/interface_board_libs/shift_register.py b/rpi-scripts/interface_board_libs/shift_register.py new file mode 100644 index 0000000..1745798 --- /dev/null +++ b/rpi-scripts/interface_board_libs/shift_register.py @@ -0,0 +1,51 @@ +import RPi.GPIO as GPIO +from time import sleep + + +class ShiftRegister: + def __init__(self, data_pin, latch_pin, clock_pin): + self.data_pin = data_pin + self.latch_pin = latch_pin + self.clock_pin = clock_pin + self.current_byte = 0 # Tracks the current state of the shift register + + GPIO.setmode(GPIO.BCM) + GPIO.setup(self.data_pin, GPIO.OUT) + GPIO.setup(self.latch_pin, GPIO.OUT) + GPIO.setup(self.clock_pin, GPIO.OUT) + + def write_byte(self, byte): + """Writes an 8-bit value to the shift register.""" + GPIO.output(self.latch_pin, 0) + for i in range(8): + GPIO.output(self.clock_pin, 0) + GPIO.output(self.data_pin, (byte >> (7 - i)) & 1) # MSB first + GPIO.output(self.clock_pin, 1) + GPIO.output(self.latch_pin, 1) + self.current_byte = byte # Update the internal state + + def clear(self): + """Clears the shift register (sets all outputs to 0).""" + self.write_byte(0) + + def set_pin(self, pin, state): + """ + Sets an individual pin to HIGH (1) or LOW (0). + Pins are numbered 0-7, with 0 being the LSB (Q0) and 7 being the MSB (Q7). + """ + if not 0 <= pin <= 7: + raise ValueError("Pin must be in range 0-7.") + if state: + self.current_byte |= (1 << pin) # Set the bit to 1 + else: + self.current_byte &= ~(1 << pin) # Set the bit to 0 + self.write_byte(self.current_byte) + + + def toggle_pin(self, pin): + """Toggles the state of an individual pin.""" + if not 0 <= pin <= 7: + raise ValueError("Pin must be in range 0-7.") + self.current_byte ^= (1 << pin) # Flip the bit + self.write_byte(self.current_byte) + diff --git a/rpi-scripts/interface_board_pins.py b/rpi-scripts/interface_board_pins.py new file mode 100644 index 0000000..b5a497e --- /dev/null +++ b/rpi-scripts/interface_board_pins.py @@ -0,0 +1,113 @@ +# Pin mappings for GPIOs and other components specifically for the pi-interface-board_v1.0 + + + + +# ====================== +# === Digital Inputs === +# ====================== +# Pin mappings for digital inputs (labeled on housing as 1-8) +GPIO_DIGITAL_INPUTS = { + 0: 25, # Dig-IN_1 is connected to GPIO_25 + 1: 16, # Dig-IN_2 is connected to GPIO_16 + 2: 26, # Dig-IN_3 is connected to GPIO_26 + 3: 13, # Dig-IN_4 is connected to GPIO_13 + 4: 6, # Dig-IN_5 is connected to GPIO_6 + 5: 5, # Dig-IN_6 is connected to GPIO_5 + 6: 22, # Dig-IN_7 is connected to GPIO_22 + 7: 24, # Dig-IN_8 is connected to GPIO_24 +} + + + + +# ====================== +# === Shift Register === +# ====================== +GPIO_SHIFT_REG_DATA = 27 +GPIO_SHIFT_REG_LATCH = 17 +GPIO_SHIFT_REG_CLOCK = 4 + +# FIXME: numbering in schematic is wrong (inverse) layout / terminal order matches the shift register though (left to right -> 0-7) +# Shift Register Channel Assignments +SHIFT_REG_CHANNEL_BUZZER = 0 # Buzzer connected to shift register channel 0 +SHIFT_REG_CHANNEL_RELAY1 = 2 # Relay 1 connected to shift register channel 2 +SHIFT_REG_CHANNEL_RELAY2 = 1 # Relay 2 connected to shift register channel 1 + + + + +# =============== +# ===== ADC ===== +# =============== +# ADC IC is connected to RPI SPI interface 0 (pins below) +ADC_SPI_BUS_NUM = 0 +ADC_SPI_DEVICE_NUM = 0 +ADC_SPI_CS_PIN = 8 # SPI Chip Select for MCP3208 +# MISO_0: GPIO_9 +# MOSI_0: GPIO_10 +# SCLK_0: GPIO_11 +# CE_0: GPIO# MCP3208 (ADC) + +# Pin mappings for Terminal number to actual ADC channels, including max voltage range +ADC_TERMINALS = { + 0: {"adc_channel": 1, "max_value": 3.3, "unit": "V"}, # Terminal 0 -> ADC channel 1, max 3.3V + 1: {"adc_channel": 0, "max_value": 3.3, "unit": "V"}, # Terminal 1 -> ADC channel 0, max 3.3V + 2: {"adc_channel": 3, "max_value": 5, "unit": "V"}, # Terminal 2 -> ADC channel 3, max 5V + 3: {"adc_channel": 2, "max_value": 5, "unit": "V"}, + 4: {"adc_channel": 5, "max_value": 12, "unit": "V"}, + 5: {"adc_channel": 4, "max_value": 24, "unit": "V"}, + 6: {"adc_channel": 7, "max_value": 20, "unit": "mA"}, # Terminal 6 -> ADC channel 7, max 20mA + 7: {"adc_channel": 6, "max_value": 20, "unit": "mA"}, +} + + +# Alternative to ADC_CHANNELS list have separate constants for the channels (more intuitive) +ADC_CHANNEL_T0__0_TO_3V3 = 1 +ADC_CHANNEL_T1__0_TO_3V3 = 0 +ADC_CHANNEL_T2__0_TO_5V = 3 +ADC_CHANNEL_T3__0_TO_5V = 2 +ADC_CHANNEL_T4__0_TO_12V = 5 +ADC_CHANNEL_T5__0_TO_24V = 4 +ADC_CHANNEL_T6__0_TO_20MA = 7 +ADC_CHANNEL_T7__0_TO_20MA = 6 + + + + +# ==================== +# === SPI Terminal === +# ==================== +SPI_BUS_NUM = 1 +# MISO_1: GPIO_19 +# MOSI_1: GPIO_20 +# SCLK_1: GPIO_21 +# CE_1: GPIO_7 + + + + +# ==================== +# === I2C Terminal === +# ==================== +GPIO_I2C_SDA = 2 +GPIO_I2C_SCL = 3 + + + + +# =================== +# === PWM outputs === +# =================== +GPIO_PWM1 = 18 # RPI_PWM0 +GPIO_PWM2 = 12 # RPI_PWM0 too + + + + +# ==================== +# === UART / RS485 === +# ==================== +GPIO_UART_TX = 14 # RPI TXD +GPIO_UART_RX = 15 # RPI RXD +GPIO_UART_DIR = 23