- Overview
- Purpose
- Files
- Sample Message
- Hardware Setup
- Persistent Device Name with udev Rules
- Scripts
- Running as a Systemd Service
- Testing with Virtual Serial Ports
- Troubleshooting
- To-Do / Future Enhancements
Scripts for bridging a vending machine's serial output to modern web services. The Raspberry Pi reads the machine's serial data and forwards complete messages to a webhook, where an external workflow (e.g., n8n) parses the content and sends an email.
This project replaces the machine's aging 3G/SMS modem with a Raspberry Pi connected to the serial port. As 3G service is retired, the Pi acts as a drop-in replacement: it listens to the same AT-command driven serial traffic, extracts the meaningful message, and posts it to a webhook so an external service can deliver the content via email.
External processing is intentionally decoupled. We use n8n in our setup to split the data and send emails, but any workflow engine or webhook consumer can be used.
- README.md - Project documentation, setup, testing, and troubleshooting.
- webrequest_send.py - Reads serial, detects packets via start/end markers, applies idle timeout and max duration, and posts JSON to a webhook.
- serial_data_test.py - Real-time serial monitor that prints raw bytes, decoded text, and cleaned lines for troubleshooting.
- send_test_data.py - Sends test lines with CRLF and a configurable delay to a serial port to simulate device output.
- sample_data.txt - Example captured serial output for reference/testing.
The data we care about looks like this:
AT+WOPEN=0
ATE0
AT
AT+CMGS=<redacted>
07/11/25 - 14:40
SN NUMBER:<redacted>
TEMP 5.3
LITRI 265159.467
EURO 60544.50
AT+CMGD=1,4
ATH
AT+CMGR=1
The machine issues AT commands to open a modem session and send the payload. This project uses those AT markers to detect the start and end of a message, then posts the captured content to a webhook for downstream processing (such as emailing).
Two common ways to connect the Raspberry Pi to the vending machine's serial interface:
- Use a reputable USB–RS‑232 adapter. Adapter tested: Unitek BF‑810Y (RS‑232).
- Many vending machines expose RS‑232 via a standard DB9 connector.
- Cable type matters:
- If the vending machine presents a DTE port (like a PC), use a null‑modem (cross‑over) cable between the adapter and the machine.
- If it presents a DCE port (like a modem), use a straight‑through cable.
- Connect the adapter to the Pi's USB port; the device will appear as
/dev/ttyUSB0,/dev/ttyUSB1, etc. - Verify the port:
ls -la /dev/ttyUSB* dmesg | grep -i tty
- Baud rate: this project uses 9600 in production (recently verified; using 115200 produced only null bytes/break conditions).
- Project-specific note: this vending machine is DTE, so a null‑modem (cross‑over) cable is required.
- Run the receiver with the detected port:
python webrequest_send.py --url https://webhook.url --serial-port /dev/ttyUSB1 --baudrate 9600 --debug
- For this project: the machine is DTE → use a null‑modem (cross‑over) cable.
- If you're unsure whether another machine is DTE or DCE, try a straight‑through cable first; if you see no data, try a null‑modem cable.
- Symptom of wrong cable: port opens fine, but no incoming data appears in the monitor.
- Some machines label the DB9; if labeled "DTE", use null‑modem. If labeled "DCE", use straight‑through.
- Only use this if the vending machine outputs TTL (3.3V) UART. If it's RS‑232 (±12V), add a level shifter (e.g., MAX3232) between the machine and the Pi GPIO.
- Wiring (TTL UART):
- Pi
GPIO14(TXD) ↔ Device RX - Pi
GPIO15(RXD) ↔ Device TX - Common GND ↔ Device GND
- Never connect RS‑232 signals directly to GPIO without a level shifter.
- Pi
- Enable UART on the Pi:
sudo raspi-config # Interface Options → Serial → Disable login shell over serial, Enable serial port hardware - The UART device appears as
/dev/serial0(symlink to/dev/ttyAMA0or/dev/ttyS0depending on model):ls -la /dev/serial0
- Run the receiver using the GPIO UART:
python webrequest_send.py --url https://webhook.url --serial-port /dev/serial0 --baudrate 9600 --debug
- Determine signal type before wiring: RS‑232 (±12V) vs TTL (3.3V). RS‑232 requires an adapter or level shifter.
- Share ground between devices; avoid ground loops and long unshielded runs.
- Confirm baud rate and framing (e.g., 8N1). For this machine 9600 works; typical rates are 9600 or 115200.
- On Linux, ensure the
dialoutgroup membership for serial access:sudo usermod -a -G dialout $USER # Log out and back in
USB-to-serial adapters can appear as /dev/ttyUSB0, /dev/ttyUSB1, etc., depending on the order they're detected or which USB port is used. A udev rule creates a consistent symlink (e.g., /dev/vendpi) that always points to your adapter.
Plug in your USB-to-serial adapter and identify it:
ls -la /dev/ttyUSB*Get the device attributes (replace ttyUSB0 with your device):
udevadm info -a -n /dev/ttyUSB0 | grep -E '{idVendor}|{idProduct}|{serial}|{manufacturer}|{product}'Example output:
ATTRS{idProduct}=="23a3"
ATTRS{idVendor}=="067b"
ATTRS{manufacturer}=="Prolific Technology Inc. "
ATTRS{product}=="USB-Serial Controller "
ATTRS{serial}=="BXDYb119D15"
Note the idVendor, idProduct, and serial values for your adapter.
Create the rule file:
sudo nano /etc/udev/rules.d/99-vendpi.rulesAdd this rule (replace the values with your adapter's attributes):
# VendPi USB-to-Serial adapter (Prolific/Unitek BF-810Y)
# Creates symlink /dev/vendpi for consistent device naming
SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="23a3", ATTRS{serial}=="BXDYb119D15", SYMLINK+="vendpi", MODE="0660", GROUP="dialout"
Rule breakdown:
SUBSYSTEM=="tty"- Only matches serial/tty devicesATTRS{idVendor}/ATTRS{idProduct}- Matches your specific adapter modelATTRS{serial}- Matches this exact adapter (useful if you have multiple of the same model)SYMLINK+="vendpi"- Creates/dev/vendpisymlinkMODE="0660"- Sets read/write permissions for owner and groupGROUP="dialout"- Assigns to dialout group for user access
# Reload udev rules
sudo udevadm control --reload-rules
# Trigger rules for existing devices
sudo udevadm trigger
# Verify the symlink
ls -la /dev/vendpiExpected output:
lrwxrwxrwx 1 root root 7 Jan 15 12:00 /dev/vendpi -> ttyUSB0
Unplug and replug the adapter, or plug it into a different USB port. The /dev/vendpi symlink should always point to your adapter.
Use /dev/vendpi instead of /dev/ttyUSB0 or /dev/ttyUSB1:
python webrequest_send.py --url https://webhook.url --serial-port /dev/vendpi --baudrate 9600Reads data from a serial port and sends it as POST requests to a webhook URL.
Usage:
python webrequest_send.py --url https://webhook.url \
--serial-port /dev/ttyUSB1 --baudrate 9600 \
--start-marker "AT+CMGS" --end-marker "ATH" \
--packet-timeout 5.0 --max-packet-duration 30.0 \
--strip-nulls --debugArguments:
--url(required): Webhook URL to send POST requests to--serial-port: Serial port to read from. Default: /dev/ttyUSB0--baudrate: Baud rate for serial connection. Default: 9600--username: Username for HTTP Basic Authentication--password: Password for HTTP Basic Authentication--insecure: Disable SSL certificate verification--interval: Loop sleep in seconds to reduce CPU usage. Default: 0.01--debug: Enable real-time display of incoming serial data--start-marker: Substring indicating the start of a packet. Default:AT+CMGS--end-marker: Substring indicating the end of a packet. Default:ATH--packet-timeout: Idle timeout (seconds). If no new lines arrive for this duration while collecting, the current packet is sent. Default: 5.0--max-packet-duration: Absolute maximum duration (seconds) from the first start-marker to send, even if lines keep arriving. Prevents runaway packets when the end-marker is missing. Default: 30.0--strip-nulls: Remove null bytes (\x00) before processing--cooldown: Cooldown period (seconds) after sending a packet during which incoming data is ignored. Helps prevent duplicate messages. Default: 120.0
How detection works:
- Start when a line contains the start marker (default
AT+CMGS). - Collect all subsequent lines until a line contains the end marker (default
ATH). - If a new start marker appears mid-collection, reset the buffer to avoid mixing packets.
- Idle timeout (
--packet-timeout): if no new lines arrive while collecting for N seconds, send the partial packet. - Max duration (
--max-packet-duration): absolute cap from first start; send even if lines keep arriving. - Optional sanitization:
--strip-nullsremoves\x00before decoding/processing.
Cooldown mechanism:
- After a packet is successfully sent, a cooldown period begins (configurable via
--cooldown). - During the cooldown period, all incoming serial data is dropped and ignored.
- This prevents duplicate messages that sometimes occur when serial data arrives twice.
- The cooldown applies to all packet send scenarios: normal completion, idle timeout, and max duration.
- When
--debugis enabled, dropped data is logged with remaining cooldown time. - Cooldown is enabled by default (120.0 seconds). Disable it by setting
--cooldownto 0.
Webhook payload: The script posts JSON to the webhook:
{
"timestamp": "2025-01-19T13:45:10.251Z",
"data": "AT+WOPEN=0\nATE0\nAT\nAT+CMGS=<redacted>\n...\nAT+CMGR=1"
}Monitor and display incoming serial port data in real-time for troubleshooting.
Usage:
python serial_data_test.py --serial-port /dev/ttyUSB1 --baudrate 9600Send test data to a serial port for testing purposes.
Usage:
python send_test_data.py --serial-port /dev/ttyUSB1 --baudrate 9600 --delay 0.1Arguments:
--serial-port: Serial port to write to. Default: /dev/ttyUSB0--baudrate: Baud rate for serial connection. Default: 9600--delay: Delay between sending each line in seconds. Default: 0.1
For production deployment, run the script as a systemd service with credentials stored securely in an environment file.
Store your webhook URL and credentials in /etc/vendpi.env:
sudo nano /etc/vendpi.envAdd your configuration:
WEBHOOK_URL=https://your-webhook.url
VENDPI_USERNAME=your_username
VENDPI_PASSWORD=your_password
Restrict access to root only:
sudo chown root:root /etc/vendpi.env
sudo chmod 600 /etc/vendpi.envNote: The environment variables (WEBHOOK_URL, VENDPI_USERNAME, VENDPI_PASSWORD) are only loaded automatically when running as a systemd service. When running the Python script manually, you must pass --url, --username, and --password as command line arguments.
Create the systemd service unit:
sudo nano /etc/systemd/system/vendpi.serviceAdd the following configuration:
[Unit]
Description=VendPi Webhook Service
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=pi
EnvironmentFile=/etc/vendpi.env
Restart=always
RestartSec=10
ExecStart=/usr/bin/python3 /home/pi/VendPi/webrequest_send.py \
--serial-port /dev/vendpi \
--baudrate 9600 \
--debug \
--cooldown 120
# --- Security hardening ---
NoNewPrivileges=yes
ProtectSystem=strict
PrivateTmp=yes
RestrictSUIDSGID=yes
RestrictAddressFamilies=AF_INET AF_INET6
ReadOnlyPaths=/home/pi/VendPi
LogsDirectory=vendpi
[Install]
WantedBy=multi-user.target# Reload systemd to pick up the new service
sudo systemctl daemon-reload
# Enable the service to start on boot
sudo systemctl enable vendpi.service
# Start the service
sudo systemctl start vendpi.service# Check service status
sudo systemctl status vendpi.service
# View logs
sudo journalctl -u vendpi.service -f
# Restart the service
sudo systemctl restart vendpi.service
# Stop the service
sudo systemctl stop vendpi.serviceYou can test the scripts without physical hardware using virtual serial port pairs.
-
Install socat (if not already installed):
sudo apt-get install socat
-
Create a virtual serial port pair:
socat -d -d pty,raw,echo=0 pty,raw,echo=0
This will output something like:
2025/12/29 21:00:00 socat[12345] N PTY is /dev/pts/2 2025/12/29 21:00:00 socat[12345] N PTY is /dev/pts/3 -
In one terminal, run your receiver script:
python webrequest_send.py --url https://webhook.url --serial-port /dev/pts/2 --baudrate 9600
-
In another terminal, send test data:
python send_test_data.py --serial-port /dev/pts/3 --baudrate 9600
The data sent to /dev/pts/3 will appear on /dev/pts/2 and be processed by your script.
When testing virtual serial ports with the systemd service, you may encounter a permission denied error:
ERROR - Failed to open serial port: [Errno 13] could not open port /dev/pts/8: [Errno 13] Permission denied
Why this happens: Virtual serial ports created by socat are owned by the user who ran socat, with restricted permissions. The systemd service runs as the pi user, which doesn't have access to these ports by default.
Solution: Create the virtual ports with explicit user, group, and permissions:
socat -d -d pty,raw,echo=0,user=pi,group=dialout,mode=660 pty,raw,echo=0,user=pi,group=dialout,mode=660This creates both ports with:
- Owner:
pi - Group:
dialout - Permissions:
660(read/write for owner and group)
Then update your service to use the virtual port and restart:
sudo systemctl restart vendpi.serviceYou can also send test data directly using bash:
printf "AT+WOPEN=0\r\nATE0\r\nAT\r\nAT+CMGS=<redacted>\r\n07/11/25 - 14:40\r\nSN NUMBER:017196\r\nTEMP 5.3\r\nLITRI 265159.467\r\nEURO 60544.50\r\nAT+CMGD=1,4\r\nATH\r\nAT+CMGR=1\r\n" > /dev/pts/3-
Verify the correct serial port:
ls -la /dev/ttyUSB* -
Check permissions:
sudo usermod -a -G dialout $USER # Log out and back in for changes to take effect
-
Use the test monitor script:
python serial_data_test.py --serial-port /dev/ttyUSB1 --baudrate 9600
This will show raw bytes, decoded text, and cleaned data for debugging.
-
Verify baud rate: Make sure the baud rate matches your device (common rates: 9600, 115200)
-
Persistent device name with udev rules - Create a udev rule to give the USB-to-serial adapter a consistent symlink (e.g.,
/dev/vendpi) regardless of which USB port it's plugged into. See Persistent Device Name with udev Rules. -
Reconnection on disconnect - Add logic to detect when the USB-to-serial adapter is unplugged and automatically reconnect when it's plugged back in, rather than requiring a service restart.