commit 9daf6acd66b93180ecf9521e84ffe36f86c579f9 Author: Mortdecai Date: Thu Mar 26 19:14:30 2026 -0400 feat: initial project setup with camera control panel 3D printing project with Ender 3 V3 SE documentation, OctoPrint/Pi infrastructure docs, and PTZ camera control webapp for two TENVIS cameras proxied through Raspberry Pi to Frigate NVR. Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05f772f --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Credentials +octopi.sethpc.xzy_api_key.md +GITEA_API.md +.env + +# Symlinked templates +SESSION.default.md +CREATE_PROJECT.md +PI3BPLUS.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..842fec1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,93 @@ +# 3D Printing Project + +## Project Overview + +3D printing setup and management for two Creality Ender 3 printers, with OctoPrint remote control and IP camera monitoring via a Raspberry Pi proxy. + +## Hardware + +### Printer +| Printer | Speed | Extruder | Auto-Level | Build Volume | Hot End | +|---|---|---|---|---|---| +| Creality Ender 3 V3 SE | 250 mm/s | Sprite Direct Drive | CR Touch | 220x220x250 mm | Standard | + +### Filament Inventory +| Material | Color | Brand | Diameter | +|---|---|---|---| +| PLA | White | OVERTURE | 1.75 mm | +| PLA | Black | OVERTURE | 1.75 mm | +| PLA | Black | ELEGOO | 1.75 mm | +| PLA | Dark Blue | ELEGOO | 1.75 mm | +| PLA | Transparent | SUNLU | 1.75 mm | +| ABS | Black | Creality | 1.75 mm | +| PETG | Red | Creality | 1.75 mm | +| PETG | Black | Creality | 1.75 mm | + +### Accessories +- Creality Hardened Steel MK8 Nozzles (5-pack) + +## Remote Printing Infrastructure + +### Raspberry Pi 3 B+ (`seth-pi`) +- **Hostname:** seth-pi +- **OS:** Debian 13 (Raspberry Pi OS Lite) +- **User:** seth (key-only SSH, no root login) +- **wlan0:** 192.168.0.102 (main network, DHCP) — default route +- **eth0:** 192.168.0.101/24 (isolated camera router subnet) +- **WiFi SSID:** WiFrei + +### OctoPrint +- **URL:** http://octopi.sethpc.xyz (port 5000 on Pi) +- **API key:** see `octopi.sethpc.xzy_api_key.md` + +### go2rtc (v1.9.14) +Restreams both cameras with H.264 hardware transcoding and MJPEG fallback. +- **API:** http://192.168.0.102:1984 +- **RTSP:** rtsp://192.168.0.102:8554 +- **Streams:** + - `cam1` / `cam1_mjpeg` — Camera 1 (192.168.0.100) + - `cam2` / `cam2_mjpeg` — Camera 2 (192.168.0.103) + +### IP Cameras — TENVIS (on isolated eth0 subnet) +Both TENVIS cameras use credentials `admin:admin` and expose RTSP on port 554 (`/11` path), HTTP on 80, and ONVIF on 8080. +- **Internal model:** C9F0SeZ0N0P4L0 (Hi3510-based SoC) +- **Firmware:** V9.1.6.1.24-20170925 +- **Hardware version:** V1.0.0.1 +- **CGI interface:** `/cgi-bin/hi3510/param.cgi` +- **cam2 has an SD card inserted** (59GB) + +| Camera | Isolated IP | Proxy Ports (on .102) | +|---|---|---| +| cam1 | 192.168.0.100 | HTTP:18080, ONVIF:18081, RTSP:18554 | +| cam2 | 192.168.0.103 | HTTP:28080, ONVIF:28081, RTSP:28554 | + +### Proxy Architecture +The Pi bridges two networks: +- **eth0** (192.168.0.101) connects to an isolated router with the two IP cameras +- **wlan0** (192.168.0.102) connects to the main homelab network +- **socat** systemd services proxy each camera port from the isolated subnet to the main network +- Port scheme: `1xxxx` = cam1, `2xxxx` = cam2 + +Proxy services (all systemd, auto-restart): +- `ipcam-proxy-http.service` — 18080 -> 192.168.0.100:80 +- `ipcam-proxy-onvif.service` — 18081 -> 192.168.0.100:8080 +- `ipcam-proxy-rtsp.service` — 18554 -> 192.168.0.100:554 +- `ipcam2-proxy-http.service` — 28080 -> 192.168.0.103:80 +- `ipcam2-proxy-onvif.service` — 28081 -> 192.168.0.103:8080 +- `ipcam2-proxy-rtsp.service` — 28554 -> 192.168.0.103:554 + +## Camera Control Panel +- **URL:** http://192.168.0.220:8090 +- **Location:** `/opt/cam-control/server.py` on CT 241 (Frigate) +- **Service:** `cam-control.service` (systemd, enabled) +- **Features:** Live MJPEG feeds, PTZ D-pad (continuous/step), speed control, flip/mirror toggles +- **Source:** `./cam-control/server.py` + +## Gitea +- **Repo:** https://git.sethpc.xyz/Seth/3d-printing +- **Remote:** `https://Seth:REDACTED_GITEA_TOKEN@git.sethpc.xyz/Seth/3d-printing.git` + +## Conventions +- Camera credentials: admin/admin (default, isolated network only) +- Pi SSH: key-only as `seth`, no password auth +- Internal password for homelab services: REDACTED_PASSWORD diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..53bfb7f --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,70 @@ +I have a Creality Ender 3 V3 SE 3D Printer, 250mm/s Faster FDM 3D Printers with CR Touch Auto Leveling, Sprite Direct Extruder Auto-Load Filament Dual Z-axis & Y-axis, Printing Size 8.66 * 8.66 * 9.84 inch https://www.amazon.com/dp/B0DD7F2BH9?ref_=ppx_hzsearch_conn_dt_b_fed_asin_title_6&th=1 +OVERTURE PLA Filament 1.75mm PLA 3D Printer Filament, 1kg Cardboard Spool (2.2lbs), Dimensional Accuracy +/- 0.02mm, Fit Most FDM Printer (White 1-Pack)View order detailsOrdered on November 21, 2024 +OVERTURE PLA Filament 1.75mm PLA 3D Printer Filament, 1kg Cardboard Spool (2.2lbs), Dimensional Accuracy +/- 0.02mm, Fit Most FDM Printer (White 1-Pack) + + +Buy it again +View your item +Creality ABS Filament 1.75mm, 3D Printer Filament, Excellent Resistance, Odorless Non-Toxic, Stability, Tough, 1kg(2.2lbs) Printing Filament for 3D Printer (Black) +View order detailsOrdered on August 9, 2024 +Creality ABS Filament 1.75mm, 3D Printer Filament, Excellent Resistance, Odorless Non-Toxic, Stability, Tough, 1kg(2.2lbs) Printing Filament for 3D Printer (Black) + + +Buy it again +View your item +Official Creality PETG 3D Printer Filament 1.75mm 1KG (2.2lbs), High Precision, Strong Toughness, Odorless, Better Flow, Moistureproof 3D Printing CR PETG Filament, Red +View order detailsOrdered on August 9, 2024 +Official Creality PETG 3D Printer Filament 1.75mm 1KG (2.2lbs), High Precision Strong Toughness, Odorless Better Flow Moistureproof 3D Printing Ender Filament(Red) + + +Buy it again +View your item +Official Creality PETG 3D Printer Filament 1.75mm 1KG (2.2lbs), High Precision, Strong Toughness, Odorless, Better Flow, Moistureproof 3D Printing CR PETG Filament, Black +View order detailsOrdered on August 9, 2024 +Official Creality PETG 3D Printer Filament 1.75mm 1KG (2.2lbs), High Precision Strong Toughness, Odorless Better Flow Moistureproof 3D Printing Ender Filament(Black) + + +Buy it again +View your item +SUNLU 3D Printer Filament PLA Filament 1.75mm, Neatly Wound 3D Printing Filament 1.75mm, Dimensional Accuracy +/- 0.02 mm, Fit Most FDM 3D Printers, 1kg Spool (2.2lbs), Transparent, Clear PLA +View order detailsOrdered on August 9, 2024 +SUNLU 3D Printer Filament 1.75mm, Neatly Wound 3D Printing Filament, +/- 0.02 mm Dimensional Accuracy, Fits Most FDM Printers, 1kg Spool, Transparent PLA + + +Buy it again +View your item +ELEGOO PLA Filament 1.75mm Black 1KG, 3D Printer Filament Dimensional Accuracy +/- 0.02mm, 1kg Cardboard Spool(2.2lbs) 3D Printing Filament Fits for Most FDM 3D Printers +View order detailsOrdered on August 8, 2024 +ELEGOO PLA Filament 1.75mm Black 1KG, 3D Printer Filament Dimensional Accuracy +/- 0.02mm, 1kg Cardboard Spool(2.2lbs) 3D Printing Filament Fits for Most FDM 3D Printers + + +Buy it again +View your item +ELEGOO PLA Filament 1.75mm Dark Blue 1KG, 3D Printer Filament Dimensional Accuracy +/- 0.02mm, 1kg Cardboard Spool(2.2lbs) 3D Printing Filament Fits for Most FDM 3D Printers +View order detailsOrdered on August 8, 2024 +ELEGOO PLA Filament 1.75mm Dark Blue 1KG, 3D Printer Filament Dimensional Accuracy +/- 0.02mm, 1kg Cardboard Spool(2.2lbs) 3D Printing Filament Fits for Most FDM 3D Printers + + +Buy it again +View your item +Creality 5 Packs Hardened Steel MK8 Nozzle for 3D Printers with High Temperature Resistance Upgraded Tungsten All Metal Nozzle Ends for Makerbot Ender 3 Ender-3 pro S1 CR-10 Series +View order detailsOrdered on August 5, 2024 +Creality 5 Packs Hardened Steel MK8 Nozzle for 3D Printers with High Temperature Resistance Upgraded Tungsten All Metal Nozzle Ends for Makerbot Ender 3 Ender-3 pro S1 CR-10 Series + + +Buy it again +View your item +OVERTURE PLA Filament 1.75mm, Neatly Wound 3D Printer Filament 1kg Spool (2.2lbs), Dimensional Accuracy +/- 0.02 mm, Fit Most FDM 3D Printers (Black 1-Pack) +View order detailsOrdered on August 5, 2024 +OVERTURE PLA Filament 1.75mm PLA 3D Printer Filament, 1kg Cardboard Spool (2.2lbs), Dimensional Accuracy +/- 0.02mm, Fit Most FDM Printer (Black 1-Pack) + + +Buy it again +View your item +Creality Ender 3 S1 3D Printer with Direct Drive Extruder CR Touch Auto Leveling High Precision Double Z-axis Screw Silent Board Printing Size 8.6X8.6X10.6in, Upgrade Ender 3 V2 for Beginners +View order detailsOrdered on August 5, 2024 +Creality Ender 3 S1 Pro with 200mm/s Printing Speed, Sprite Direct Extruder CR Touch Auto Leveling 300℃ High-Temp Printing, Dual Z-axis Screw Printing Size 8.66 * 8.66 * 10.63'' + + +Buy it again +View your item diff --git a/SESSION.md b/SESSION.md new file mode 100644 index 0000000..0f9887c --- /dev/null +++ b/SESSION.md @@ -0,0 +1,81 @@ +# SESSION.md — 3D Printing Project + +## Context Files + +- `/root/bin/core_homelab.md` — cluster topology, SSH aliases +- `/root/bin/services_directory.md` — active service IPs and domains +- `/root/bin/SESSION.md` — global session memory +- `./CONTEXT.md` — printer hardware, filament inventory, and accessories +- `./GITEA_API.md` — Gitea push credentials and commit convention + +## Project Summary + +3D printing setup and management for a Creality Ender 3 homelab. Covers two printers (Ender 3 V3 SE and Ender 3 S1 Pro), filament inventory across PLA/PETG/ABS in multiple colors, and hardened steel MK8 nozzles. The CONTEXT.md file serves as an inventory of purchased hardware and consumables. + +## Memory Discipline + +- Update `SESSION.md` immediately when a durable fact, decision, or fix is discovered. +- Before every final reply, run a memory check and append any missing durable notes. +- End every reply with one line: `Session memory: updated` or `Session memory: no new durable facts.` + +## Hardware + +### Printer +| Printer | Speed | Extruder | Auto-Level | Build Volume | +|---|---|---|---|---| +| Creality Ender 3 V3 SE | 250 mm/s | Sprite Direct Drive | CR Touch | 220 x 220 x 250 mm | + +### Filament Inventory +| Material | Color | Brand | Diameter | +|---|---|---|---| +| PLA | White | OVERTURE | 1.75 mm | +| PLA | Black | OVERTURE | 1.75 mm | +| PLA | Black | ELEGOO | 1.75 mm | +| PLA | Dark Blue | ELEGOO | 1.75 mm | +| PLA | Transparent | SUNLU | 1.75 mm | +| ABS | Black | Creality | 1.75 mm | +| PETG | Red | Creality | 1.75 mm | +| PETG | Black | Creality | 1.75 mm | + +### Accessories +- Creality Hardened Steel MK8 Nozzles (5-pack) — for abrasive filaments + +## Gitea + +- Repo: https://git.sethpc.xyz/Seth/3d-printing +- Remote: `https://Seth:REDACTED_GITEA_TOKEN@git.sethpc.xyz/Seth/3d-printing.git` +- API key: see `./GITEA_API.md` + +## Session Notes + +### Initial setup (2026-03-17) +- Created SESSION.md from template in SESSION.default.md +- Organized CONTEXT.md inventory data into structured hardware/filament tables + +### Camera/Pi discovery (2026-03-24) +- Pi 3B+ (`seth-pi`) found at 192.168.0.102 (wlan0) / 192.168.0.101 (eth0) +- eth0 connects to isolated router with 2 IP cameras (192.168.0.100, 192.168.0.103) +- 6 socat proxy services bridge camera HTTP/RTSP/ONVIF to main network +- go2rtc v1.9.14 restreams both cameras (H.264 hw + MJPEG) +- OctoPrint running on port 5000 +- Camera creds: admin/admin (default, isolated subnet) +- Created CLAUDE.md with full infrastructure documentation +- [x] Document OctoPrint setup — done, documented in CLAUDE.md +- Camera make: TENVIS (both cameras) + +### Camera control panel (2026-03-24) +- Built single-page PTZ control webapp (`cam-control/server.py`) +- Deployed to Frigate CT 241 at http://192.168.0.220:8090 +- systemd service `cam-control.service` enabled and running +- Features: dual MJPEG feeds via go2rtc, PTZ D-pad, speed, flip/mirror + +### OctoPrint timelapse fix (2026-03-24) +- Pi fstab pointed to pve197 for tank SMB — updated to node-173 post-migration +- Created octoprint Samba user on node-173 (creds: octoprint/octoprint) +- Timelapses dir at /tank/Timelapses now accessible, OctoPrint restarted + +### Open threads +- [x] Identified camera: TENVIS, internal model C9F0SeZ0N0P4L0 (Hi3510 SoC), firmware V9.1.6.1.24-20170925 +- [ ] Add slicer profiles and recommended print settings per filament type +- [ ] Set up Gitea repo and push project files +- [ ] Change default camera passwords (admin/admin) diff --git a/cam-control/server.py b/cam-control/server.py new file mode 100644 index 0000000..875823e --- /dev/null +++ b/cam-control/server.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python3 +"""PTZ Camera Control Panel for two TENVIS cameras proxied via Raspberry Pi.""" + +import json +import urllib.request +import urllib.parse +from http.server import HTTPServer, BaseHTTPRequestHandler +from socketserver import ThreadingMixIn + +CAMERAS = { + "cam1": {"host": "192.168.0.102", "http_port": 18080, "label": "Printer Cam 1"}, + "cam2": {"host": "192.168.0.102", "http_port": 28080, "label": "Printer Cam 2"}, +} +GO2RTC = "127.0.0.1:1984" +AUTH = ("admin", "admin") +PORT = 8090 + +HTML = """ + + + + +Camera Control + + + +

Camera Control Panel

+
+ + +""" + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + path = self.path.split("?")[0] + query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query) + + if path == "/": + self._respond(200, "text/html", HTML) + return + + # /api//stream -> proxy go2rtc MJPEG + parts = path.strip("/").split("/") + if len(parts) >= 3 and parts[0] == "api" and parts[1] in CAMERAS: + cam_id = parts[1] + cam = CAMERAS[cam_id] + action = parts[2] + + if action == "stream": + self._proxy_stream(cam_id) + return + elif action == "frame": + self._proxy_frame(cam_id) + return + elif action == "ptz": + act = query.get("act", ["stop"])[0] + step = query.get("step", ["0"])[0] + resp = self._cam_cgi(cam, f"cmd=ptzctrl&-step={step}&-act={act}") + self._respond(200, "text/plain", resp) + return + elif action == "speed": + pan = query.get("pan", ["2"])[0] + tilt = query.get("tilt", ["2"])[0] + resp = self._cam_cgi(cam, f"cmd=setptzspeed&-panspeed={pan}&-tiltspeed={tilt}") + self._respond(200, "text/plain", resp) + return + elif action == "image": + params = "&".join(f"-{k}={v[0]}" for k, v in query.items()) + resp = self._cam_cgi(cam, f"cmd=setimageattr&{params}") + self._respond(200, "text/plain", resp) + return + elif action == "imageattr": + resp = self._cam_cgi(cam, "cmd=getimageattr") + resp2 = self._cam_cgi(cam, "cmd=getptzspeed") + self._respond(200, "text/plain", resp + "\n" + resp2) + return + + self._respond(404, "text/plain", "Not found") + + def _cam_cgi(self, cam, params): + url = f"http://{cam['host']}:{cam['http_port']}/cgi-bin/hi3510/param.cgi?{params}" + try: + req = urllib.request.Request(url) + cred = f"{AUTH[0]}:{AUTH[1]}" + import base64 + req.add_header("Authorization", "Basic " + base64.b64encode(cred.encode()).decode()) + with urllib.request.urlopen(req, timeout=5) as resp: + return resp.read().decode() + except Exception as e: + return f"Error: {e}" + + def _proxy_frame(self, cam_id): + url = f"http://{GO2RTC}/api/frame.jpeg?src=printer_{cam_id}" + try: + req = urllib.request.Request(url) + with urllib.request.urlopen(req, timeout=5) as resp: + data = resp.read() + self.send_response(200) + self.send_header("Content-Type", "image/jpeg") + self.send_header("Content-Length", str(len(data))) + self.send_header("Cache-Control", "no-cache, no-store") + self.end_headers() + self.wfile.write(data) + except Exception as e: + self._respond(502, "text/plain", f"Frame error: {e}") + + def _proxy_stream(self, cam_id): + url = f"http://{GO2RTC}/api/stream.mjpeg?src=printer_{cam_id}" + try: + req = urllib.request.Request(url) + resp = urllib.request.urlopen(req, timeout=10) + content_type = resp.headers.get("Content-Type", "multipart/x-mixed-replace") + self.send_response(200) + self.send_header("Content-Type", content_type) + self.send_header("Cache-Control", "no-cache") + self.end_headers() + while True: + chunk = resp.read(8192) + if not chunk: + break + self.wfile.write(chunk) + except (BrokenPipeError, ConnectionResetError): + pass + except Exception as e: + self._respond(502, "text/plain", f"Stream error: {e}") + + def _respond(self, code, content_type, body): + data = body.encode() if isinstance(body, str) else body + self.send_response(code) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + + def log_message(self, fmt, *args): + pass # suppress request logs + + +class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): + daemon_threads = True + + +if __name__ == "__main__": + server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler) + print(f"Camera Control Panel running on http://0.0.0.0:{PORT}") + server.serve_forever()