diff --git a/PLAN.md b/PLAN.md index bec649f..16b9638 100644 --- a/PLAN.md +++ b/PLAN.md @@ -30,8 +30,14 @@ pick up the project without needing prior chat history. - **The SIM868 module chip itself needs its PWRKEY button pressed and held (~1-2s) to power on** — this is separate from the board's 5V/PWR LED, which can be lit while the module chip is fully off. AT commands get zero response if the module chip isn't powered, even with perfect - UART wiring/config. This is a manual step needed on every power cycle unless a P4 - GPIO-power-on hardware mod (per Waveshare FAQ) is done — not pursued so far. + UART wiring/config. **Update 2026-08-09: this HAT revision DOES have the "P4" GPIO-power-on + mod wire installed** (per the Waveshare FAQ/`GSM_PWRKEY.rar` script) — confirmed by pulsing + physical header pin 7 (BCM GPIO4) LOW for 4s then HIGH while the module was unresponsive, after + which it immediately answered `AT`. This is now automated in `modules/app_gps.py` + (`_ensure_module_powered()`), which checks `AT` first and only pulses PWRKEY if the module + isn't already responding (PWRKEY is a toggle — pulsing an already-on module would turn it back + off, so this check-first behavior is required). No more manual button-pressing needed after a + Pi power cycle. - Confirmed AT command baud rate: **115200**. - GNSS workflow: `AT+CGNSPWR?` to check GNSS power state, `AT+CGNSPWR=1` to power on, `AT+CGNSINF` to poll for a fix. Response format: @@ -577,7 +583,18 @@ optimization. Not scoped/designed yet. `~/app/yaris-obd/yaris-obd.log` (not journald - the systemd unit redirects both streams there), so use `tail`/`grep` on that file, not `journalctl`, to see application-level prints (upload progress, OBD/GNSS errors, etc.) - `journalctl -u yaris-obd.service` only shows service - start/stop/crash lifecycle events. + start/stop/crash lifecycle events. **Update 2026-08-09**: plain `print()` calls were silently + invisible in that log for months because Python fully-buffers stdout when it's not a TTY (a + file, in this case) — `logging`-based lines (e.g. the `obd` library's own logger, which uses + stderr) showed up fine, but bare `print()` diagnostics from `app_gps.py` never did. Fixed by + adding `Environment=PYTHONUNBUFFERED=1` to the systemd unit + (`deploy/yaris-obd.service`) — **remember to also copy the updated unit file to + `/etc/systemd/system/yaris-obd.service` and run `sudo systemctl daemon-reload`** on the Pi, a + plain `git pull` + restart does NOT pick up systemd unit file changes (the live + `/etc/systemd/system/` copy is independent of the repo's `deploy/` copy). +- **Pi deploy loop, requirements.txt changes**: after any `requirements.txt` change (e.g. adding + `RPi.GPIO`), also run `~/app/yaris-obd/venv/bin/pip install -r requirements.txt` on the Pi — a + `git pull` alone doesn't install new dependencies into the venv. - **Pi memory budget**: ~900MB usable RAM, no swap. Any code path that might buffer a whole dashcam-sized file (hundreds of MB) in memory is unsafe on this hardware - stream, don't buffer (see Phase 10). diff --git a/deploy/yaris-obd.service b/deploy/yaris-obd.service index ca79fb1..d608685 100644 --- a/deploy/yaris-obd.service +++ b/deploy/yaris-obd.service @@ -8,6 +8,10 @@ Type=simple User=yaris Group=yaris WorkingDirectory=/home/yaris/app/yaris-obd +# Unbuffered stdout so print() diagnostics (GPS/GNSS status, etc.) actually reach +# the log file promptly instead of sitting in Python's stdout buffer indefinitely +# when it's redirected to a file instead of a TTY. +Environment=PYTHONUNBUFFERED=1 ExecStart=/home/yaris/app/yaris-obd/venv/bin/python app.py Restart=on-failure RestartSec=5 diff --git a/modules/app_gps.py b/modules/app_gps.py index 21d95a5..eae16e5 100644 --- a/modules/app_gps.py +++ b/modules/app_gps.py @@ -3,12 +3,28 @@ from threading import Thread, Lock import serial import time +try: + import RPi.GPIO as GPIO +except (ImportError, RuntimeError): + # Not running on a Pi (e.g. local dev machine) - PWRKEY auto power-on is + # simply unavailable there; _pulse_pwrkey() below degrades gracefully. + GPIO = None + # SIM868 GNSS/GSM/GPRS HAT is wired to the Pi's GPIO UART (jumper set to "B: Pi-SIM868"), # exposed as /dev/serial0 once the Pi's onboard Bluetooth has been disabled/freed. SERIAL_PORT = "/dev/serial0" BAUDRATE = 115200 POLL_INTERVAL = 2 # seconds between AT+CGNSINF polls +# This HAT revision has the "P4" GPIO power-on mod wire connected to the SIM868's +# PWRKEY line (confirmed 2026-08-09 by pulsing it and seeing the module respond to +# AT afterward) - physical header pin 7 (BCM GPIO4), same pin Waveshare's own +# GSM_PWRKEY.py reference script uses. This lets us auto power-on the module chip +# on every app start instead of requiring someone to press its PWRKEY button by +# hand after every Pi power cycle. +PWRKEY_PIN = 7 # BOARD numbering +PWRKEY_HOLD_SECONDS = 4 # matches Waveshare's reference script + LocationX = None LocationY = None @@ -40,6 +56,65 @@ def _send_at(cmd, wait=0.5): return ser.read(1000).decode(errors="replace") +def _module_responds(): + """Check whether the SIM868 module chip is powered on and answering AT commands.""" + try: + return "OK" in _send_at("AT", wait=0.3) + except Exception: + return False + + +def _pulse_pwrkey(): + """Pulse the PWRKEY line low then high, same sequence as Waveshare's own + GSM_PWRKEY.py reference script. + + PWRKEY is a toggle, not a simple "power on" switch: pressing it while the + module is off powers it on, but pressing it again while already on powers + it back off. Callers must only invoke this after confirming via + `_module_responds()` that the module is NOT currently responding - never + unconditionally on every app start, or a normal restart while the module + is already up would accidentally shut it back down. + """ + if GPIO is None: + print("RPi.GPIO not available, cannot auto-power-on the SIM868 module - " + "press its PWRKEY button by hand.") + return + GPIO.setmode(GPIO.BOARD) + GPIO.setup(PWRKEY_PIN, GPIO.OUT) + GPIO.output(PWRKEY_PIN, GPIO.LOW) + time.sleep(PWRKEY_HOLD_SECONDS) + GPIO.output(PWRKEY_PIN, GPIO.HIGH) + GPIO.cleanup(PWRKEY_PIN) + + +def _ensure_module_powered(): + """Power on the SIM868 module chip via its PWRKEY line if it isn't already on. + + The HAT's 5V rail can be live (board powered) while the module chip itself + stays fully off until PWRKEY is pulsed - this is what previously required a + manual button press after every Pi power cycle. Returns True once the + module is confirmed responding to AT commands, False if it still isn't + after attempting the power-on pulse (e.g. GPIO unavailable, or this board + doesn't have the P4 mod wire). + """ + if _module_responds(): + return True + + print("SIM868 module not responding to AT, pulsing PWRKEY to power it on...") + _pulse_pwrkey() + time.sleep(3) # give the module a few seconds to boot after PWRKEY release + + for _ in range(5): + if _module_responds(): + print("SIM868 module powered on and responding.") + return True + time.sleep(1) + + print("SIM868 module still not responding after PWRKEY pulse - check the " + "P4 wiring or press the physical PWRKEY button.") + return False + + def _ensure_gnss_powered(): resp = _send_at("AT+CGNSPWR?") if "+CGNSPWR: 0" in resp: @@ -76,7 +151,8 @@ def _poll_loop(): global LocationX, LocationY try: with _serial_lock: - _ensure_gnss_powered() + if _ensure_module_powered(): + _ensure_gnss_powered() except Exception as e: print("Failed to initialize GNSS:", str(e)) diff --git a/requirements.txt b/requirements.txt index 7c39194..15db4d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ gevent pymongo python-dotenv pyserial +RPi.GPIO obd requests requests-toolbelt \ No newline at end of file