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
@@ -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
+
+
---
+
+
# 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