diff --git a/go.mod b/go.mod index 10dd764..e30b089 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/cubecraft/remoterig -go 1.25.0 +go 1.19 require ( github.com/eclipse/paho.mqtt.golang v1.5.0 @@ -12,9 +12,12 @@ require ( require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect modernc.org/libc v1.72.3 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index 438a07b..320629e 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,15 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= +github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= @@ -15,30 +17,24 @@ github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= -modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= -modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= -modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= -modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= -modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= -modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= @@ -46,12 +42,8 @@ modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJ modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= -modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= -modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w= modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= -modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/hardware/case/case-body-v3.stl b/hardware/case/case-body-v3.stl deleted file mode 100644 index af38598..0000000 Binary files a/hardware/case/case-body-v3.stl and /dev/null differ diff --git a/hardware/case/case-lid-v3.stl b/hardware/case/case-lid-v3.stl deleted file mode 100644 index 8a25375..0000000 Binary files a/hardware/case/case-lid-v3.stl and /dev/null differ diff --git a/hardware/case/export-case-body-v3.scad b/hardware/case/export-case-body-v3.scad deleted file mode 100644 index a91c74a..0000000 --- a/hardware/case/export-case-body-v3.scad +++ /dev/null @@ -1,2 +0,0 @@ -include ; -render(convexity=10) case_body(); diff --git a/hardware/case/export-case-lid-v3.scad b/hardware/case/export-case-lid-v3.scad deleted file mode 100644 index bbb0210..0000000 --- a/hardware/case/export-case-lid-v3.scad +++ /dev/null @@ -1,2 +0,0 @@ -include ; -render(convexity=10) case_lid(); diff --git a/hardware/case/export-full-case-preview-v3.scad b/hardware/case/export-full-case-preview-v3.scad deleted file mode 100644 index d669f1d..0000000 --- a/hardware/case/export-full-case-preview-v3.scad +++ /dev/null @@ -1,2 +0,0 @@ -include ; -render(convexity=10) full_case(); diff --git a/hardware/case/export-tripod-clamp-v3.scad b/hardware/case/export-tripod-clamp-v3.scad deleted file mode 100644 index a11ffd5..0000000 --- a/hardware/case/export-tripod-clamp-v3.scad +++ /dev/null @@ -1,2 +0,0 @@ -include ; -render(convexity=10) tripod_clamp(); diff --git a/hardware/case/full-case-preview-v3.stl b/hardware/case/full-case-preview-v3.stl deleted file mode 100644 index 70ced51..0000000 Binary files a/hardware/case/full-case-preview-v3.stl and /dev/null differ diff --git a/hardware/case/tripod-case-v3.scad b/hardware/case/tripod-case-v3.scad deleted file mode 100644 index 586f344..0000000 --- a/hardware/case/tripod-case-v3.scad +++ /dev/null @@ -1,217 +0,0 @@ -// RemoteRig — Dual-ESP Tripod Case v3 -// v3 changes: screw-tightened tripod clamp + dovetail slide interface. -// Coordinate system: all case/lid geometry uses bottom-origin Z. - -$fn = 36; - -// Board dimensions -esp8266_w = 34.2; esp8266_d = 25.6; esp8266_h = 5; -esp32_w = 52; esp32_d = 28; esp32_h = 5; -board_gap = 3; -stack_h = esp8266_h + esp32_h + board_gap; -inner_w = max(esp8266_w, esp32_w); -inner_d = max(esp8266_d, esp32_d); -inner_h = stack_h + 2; - -// Case parameters -wall = 2.0; -tol = 0.4; -outer_w = inner_w + wall*2 + tol*2; // 56.8mm -outer_d = inner_d + wall*2 + tol*2; // 32.8mm -outer_h = inner_h + wall*2; // 19mm -corner_r = 2.5; - -// Tripod clamp parameters -pole_dia = 35; // nominal stand/pole diameter -clamp_thick = 4.0; // ring wall thickness -clamp_width = 16.0; // extrusion width along Z -mouth_width = 13.0; // clamp opening -m3_clearance = 3.4; // M3 screw clearance -nut_flat = 6.4; // M3 nut trap flat-to-flat - -// Dovetail slide interface -// Male rail is on the case; matching female socket is on the tripod clamp. -// This is easier to inspect and avoids the previous mismatched "two lips + tab" geometry. -rail_z = outer_h * 0.78; -rail_depth = 5.0; -rail_neck_w = 12.0; // narrow width at case wall / slot opening -rail_outer_w = 18.0; // wider retained edge -rail_clearance = 0.45; // FDM sliding clearance per side-ish -socket_wall = 2.2; - -// Cable ports -usb_port_w = 12; usb_port_h = 6; -uart_port_w = 6; uart_port_h = 4; - -// Uncomment one for manual OpenSCAD use -// full_case(); -// case_body(); -// case_lid(); -// tripod_clamp(); - -module rounded_cube_centered(w, d, h, r) { - hull() { - for (x = [-1, 1], y = [-1, 1], z = [-1, 1]) { - translate([x*(w/2 - r), y*(d/2 - r), z*(h/2 - r)]) - sphere(r=r, $fn=24); - } - } -} - -module rounded_cube0(w, d, h, r) { - translate([0, 0, h/2]) rounded_cube_centered(w, d, h, r); -} - -module hex_prism(d, h) { - cylinder(d=d, h=h, center=true, $fn=6); -} - -module dovetail_prism(length_z, front_w, back_w, depth) { - // 2D profile is X/Y, extruded along Z. - rotate([0, 0, 0]) - linear_extrude(height=length_z, center=true, convexity=10) - polygon(points=[ - [-front_w/2, 0], [front_w/2, 0], - [back_w/2, depth], [-back_w/2, depth] - ]); -} - -module case_shell() { - difference() { - rounded_cube0(outer_w, outer_d, outer_h, corner_r); - - // Open internal cavity: starts above bottom wall, extends past top. - translate([0, 0, wall]) - rounded_cube0(inner_w + tol, inner_d + tol, outer_h + 2, 1.6); - - // USB power IN / OUT ports through front/back walls. - translate([0, outer_d/2 + 0.1, wall + 4]) - cube([usb_port_w, wall*3, usb_port_h], center=true); - translate([0, -outer_d/2 - 0.1, wall + 4]) - cube([usb_port_w, wall*3, usb_port_h], center=true); - - // UART side channel. - translate([outer_w/2 + 0.1, 0, wall + 6]) - cube([wall*3, uart_port_w, uart_port_h], center=true); - - // LED viewing window on front lower wall. - translate([-outer_w/4, -outer_d/2 - 0.1, wall + 2]) - cube([6, wall*2, 3], center=true); - } -} - -module screw_post(x, y) { - difference() { - translate([x, y, wall]) cylinder(d=5.0, h=outer_h-wall-0.5, center=false, $fn=24); - translate([x, y, wall-0.5]) cylinder(d=2.1, h=outer_h+1, center=false, $fn=20); - } -} - -module case_male_dovetail_rail() { - // Positive tapered rail on the case back. Cross-section is narrow at the - // wall and wider at the outside, so the clamp socket captures it. - translate([0, outer_d/2 - 0.15, outer_h/2]) - dovetail_prism(rail_z, rail_neck_w, rail_outer_w, rail_depth); - - // Bottom stop so the clamp socket cannot slide past the case. - translate([0, outer_d/2 + rail_depth/2, outer_h*0.12]) - rounded_cube_centered(rail_outer_w + 3.0, rail_depth + 0.8, 2.4, 0.8); -} - -module case_body() { - union() { - case_shell(); - for (x = [-1, 1], y = [-1, 1]) - screw_post(x*(outer_w/2 - 5), y*(outer_d/2 - 5)); - case_male_dovetail_rail(); - } -} - -module case_lid() { - difference() { - rounded_cube0(outer_w, outer_d, wall*2, 1.8); - - for (x = [-1, 1], y = [-1, 1]) { - translate([x*(outer_w/2 - 5), y*(outer_d/2 - 5), -0.5]) - cylinder(d=2.4, h=wall*2 + 1, center=false, $fn=20); - } - - for (x = [-outer_w/4, 0, outer_w/4]) { - translate([x, 0, wall*2/2]) - cube([8, outer_d*0.6, wall*3], center=true); - } - } -} - -module clamp_ring_with_mouth() { - outer_r = pole_dia/2 + clamp_thick; - difference() { - cylinder(r=outer_r, h=clamp_width, center=true, $fn=72); - cylinder(r=pole_dia/2 + rail_clearance, h=clamp_width + 1, center=true, $fn=72); - // Mouth opens toward +Y. Width is intentionally generous for snap-on placement before tightening. - translate([0, outer_r, 0]) - cube([mouth_width, outer_r*2, clamp_width + 2], center=true); - } -} - -module clamp_ears() { - outer_r = pole_dia/2 + clamp_thick; - ear_y = outer_r + 2.2; - ear_z = 0; - difference() { - union() { - translate([-mouth_width/2 - 3.2, ear_y, ear_z]) - rounded_cube_centered(7.0, 9.0, clamp_width, 1.4); - translate([ mouth_width/2 + 3.2, ear_y, ear_z]) - rounded_cube_centered(7.0, 9.0, clamp_width, 1.4); - } - // M3 screw passes across the mouth along X. - translate([0, ear_y, ear_z]) - rotate([0, 90, 0]) cylinder(d=m3_clearance, h=mouth_width + 24, center=true, $fn=24); - // Nut trap on the right ear. - translate([mouth_width/2 + 3.2, ear_y, ear_z]) - rotate([0, 90, 0]) hex_prism(nut_flat, 4.2); - } -} - -module clamp_dovetail_socket() { - outer_r = pole_dia/2 + clamp_thick; - socket_outer_w = rail_outer_w + socket_wall*2; - socket_depth = rail_depth + socket_wall*2; - - // Solid boss on the rear of the clamp, opposite the tightening mouth. - // A matching dovetail void is cut through it along Z so the case rail - // slides in from the top/bottom with practical FDM clearance. - difference() { - translate([0, -outer_r - socket_depth/2 + socket_wall, 0]) - rounded_cube_centered(socket_outer_w, socket_depth, clamp_width, 1.2); - - translate([0, -outer_r - 0.15, 0]) - dovetail_prism( - clamp_width + 1.0, - rail_neck_w + rail_clearance, - rail_outer_w + rail_clearance, - rail_depth + 0.6 - ); - } -} - -module tripod_clamp() { - union() { - clamp_ring_with_mouth(); - clamp_ears(); - clamp_dovetail_socket(); - } -} - -// Backward-compatible alias for earlier export scripts. -module tripod_clip() { - tripod_clamp(); -} - -module full_case() { - case_body(); - translate([0, 0, outer_h + 2]) case_lid(); - translate([0, outer_d/2 + pole_dia/2 + clamp_thick + 8, outer_h/2]) - rotate([90, 0, 0]) tripod_clamp(); -} diff --git a/hardware/case/tripod-case.scad b/hardware/case/tripod-case.scad deleted file mode 100644 index 9780a3a..0000000 --- a/hardware/case/tripod-case.scad +++ /dev/null @@ -1,201 +0,0 @@ -// RemoteRig — Dual-ESP Tripod Case -// ================================= -// Small box that clips onto a tripod leg or light stand pole. -// Holds ESP8266 D1 Mini + ESP32 Dev Board (stacked). -// Powered by standard USB battery pack. No camera sleeve needed. -// -// Print settings: -// Material: PETG | Layer: 0.2mm | Infill: 20% gyroid -// Supports: yes (for clip overhang) | Brim: 5mm - -// ── Board dimensions ── -esp8266_w = 34.2; esp8266_d = 25.6; esp8266_h = 5; -esp32_w = 52; esp32_d = 28; esp32_h = 5; -board_gap = 3; // air gap between stacked boards -stack_h = esp8266_h + esp32_h + board_gap; -inner_w = max(esp8266_w, esp32_w); -inner_d = max(esp8266_d, esp32_d); -inner_h = stack_h + 2; - -// ── Case parameters ── -wall = 2.0; -tol = 0.4; -outer_w = inner_w + wall*2 + tol*2; -outer_d = inner_d + wall*2 + tol*2; -outer_h = inner_h + wall*2; - -// ── Tripod clip parameters ── -pole_min_dia = 20; // smallest pole -pole_max_dia = 35; // largest pole -clip_width = 12; // clip width -clip_thick = 3; // clip arm thickness -clip_grip = 2; // grip ridges - -// ── Cable ports ── -usb_port_w = 12; usb_port_h = 6; -uart_port_w = 6; uart_port_h = 4; - -// ══════════════════════════════════════════════════════════════ -// MAIN — render the full case -// ══════════════════════════════════════════════════════════════ - -// Uncomment to render individual parts: -full_case(); -// case_body(); -// case_lid(); -// tripod_clip(); - -module full_case() { - case_body(); - // Lid positioned above (for visualization) - translate([0, 0, outer_h + 2]) - case_lid(); - // Clip on the back - translate([0, outer_d/2 + pole_max_dia/2 + clip_thick, outer_h/2]) - tripod_clip(); -} - -// ══════════════════════════════════════════════════════════════ -// Case Body — holds both boards, cable ports -// ══════════════════════════════════════════════════════════════ - -module case_body() { - difference() { - // Outer shell - rounded_cube(outer_w, outer_d, outer_h, 3); - - // Inner cavity - translate([0, 0, wall]) - rounded_cube(inner_w + tol, inner_d + tol, inner_h + tol, 2); - - // ── Board recesses ── - - // Bottom: ESP32 (larger board) - translate([0, 0, wall + 1]) - cube([esp32_w + tol, esp32_d + tol, esp32_h + 1], center=true); - - // Top: ESP8266 (smaller board) - translate([0, 0, wall + esp32_h + board_gap + 1]) - cube([esp8266_w + tol, esp8266_d + tol, esp8266_h + 1], center=true); - - // ── Cable ports ── - - // USB power IN (from battery pack → ESP32) - translate([0, outer_d/2, outer_h/3]) - cube([usb_port_w, wall*3, usb_port_h], center=true); - - // USB power OUT (from battery pack → GoPro) - translate([0, -outer_d/2, outer_h/3]) - cube([usb_port_w, wall*3, usb_port_h], center=true); - - // UART wire channel (ESP8266 → ESP32 internal) - translate([outer_w/2, 0, outer_h/2]) - cube([wall*3, uart_port_w, uart_port_h], center=true); - - // ── Ventilation slots (top edge) ── - for (x = [-outer_w/4, 0, outer_w/4]) { - translate([x, 0, outer_h - wall]) - cube([8, outer_d*0.6, 2], center=true); - } - - // ── Screw posts for lid ── - for (x = [-1, 1], y = [-1, 1]) { - translate([x*(outer_w/2 - 5), y*(outer_d/2 - 5), outer_h/2]) - cylinder(d=3.2, h=outer_h, center=true, $fn=16); - } - - // ── LED window (thin spot to see board LEDs) ── - translate([-outer_w/4, -outer_d/2, wall]) - cube([6, 1, 3], center=true); - } - - // ── Tripod clip mount (rail on back) ── - translate([0, outer_d/2, outer_h/2]) - rotate([90, 0, 0]) - difference() { - cube([clip_width + 4, outer_h*0.7, 6], center=true); - // T-slot for clip to slide in - cube([clip_width + 1, outer_h*0.7 + 1, 4], center=true); - } -} - -// ══════════════════════════════════════════════════════════════ -// Case Lid — snap-fit or screw-on cover -// ══════════════════════════════════════════════════════════════ - -module case_lid() { - difference() { - rounded_cube(outer_w, outer_d, wall*2, 2); - - // Screw holes (match body posts) - for (x = [-1, 1], y = [-1, 1]) { - translate([x*(outer_w/2 - 5), y*(outer_d/2 - 5), 0]) - cylinder(d=3.2, h=wall*3, center=true, $fn=16); - } - - // Ventilation slots (match body) - for (x = [-outer_w/4, 0, outer_w/4]) { - translate([x, 0, 0]) - cube([8, outer_d*0.6, 3], center=true); - } - } -} - -// ══════════════════════════════════════════════════════════════ -// Tripod Clip — C-clamp for pole mounting -// ══════════════════════════════════════════════════════════════ - -module tripod_clip() { - difference() { - union() { - // Main body - hull() { - translate([0, -pole_max_dia/2 - clip_thick, 0]) - cube([clip_width, clip_thick*2, outer_h*0.7], center=true); - - translate([0, pole_max_dia/2 + clip_thick, 0]) - cube([clip_width, clip_thick*2, outer_h*0.7], center=true); - } - - // Top arm (flexible) - translate([0, -pole_max_dia/2 - clip_thick, outer_h*0.35]) - cube([clip_width, pole_max_dia + clip_thick*4, clip_thick], center=true); - - // Bottom arm - translate([0, -pole_max_dia/2 - clip_thick, -outer_h*0.35]) - cube([clip_width, pole_max_dia + clip_thick*4, clip_thick], center=true); - - // Mounting tab (slides into case rail) - translate([0, -pole_max_dia/2 - clip_thick*3, 0]) - cube([clip_width + 1, clip_thick*2, outer_h*0.7], center=true); - } - - // Pole hole - cylinder(d=pole_max_dia + 2, h=outer_h*1.5, center=true, $fn=32); - - // Grip ridges on inner surface - for (z = [-outer_h*0.25, 0, outer_h*0.25]) { - translate([0, 0, z]) - rotate_extrude(angle=180, $fn=32) - translate([pole_max_dia/2 + 0.5, 0]) - circle(d=1); - } - - // Entry slot (pole slides in from front) - translate([0, pole_max_dia/2 + clip_thick, 0]) - cube([clip_width + 2, pole_max_dia + 10, outer_h*0.7], center=true); - } -} - -// ══════════════════════════════════════════════════════════════ -// Utility: rounded cube -// ══════════════════════════════════════════════════════════════ - -module rounded_cube(w, d, h, r) { - hull() { - for (x = [-1, 1], y = [-1, 1], z = [-1, 1]) { - translate([x*(w/2 - r), y*(d/2 - r), z*(h/2 - r)]) - sphere(r=r, $fn=20); - } - } -} diff --git a/hardware/case/tripod-clamp-v3.stl b/hardware/case/tripod-clamp-v3.stl deleted file mode 100644 index fa616d2..0000000 Binary files a/hardware/case/tripod-clamp-v3.stl and /dev/null differ diff --git a/hardware/case/viewer.html b/hardware/case/viewer.html deleted file mode 100644 index 6b9d341..0000000 --- a/hardware/case/viewer.html +++ /dev/null @@ -1,274 +0,0 @@ - - - - - -RemoteRig Case — 3D Viewer - - - - - - - diff --git a/internal/mqtt/subscriber.go b/internal/mqtt/subscriber.go index 4ea0480..3d1cb99 100644 --- a/internal/mqtt/subscriber.go +++ b/internal/mqtt/subscriber.go @@ -209,6 +209,21 @@ func (s *Subscriber) handleStatus(cameraID string, payload []byte) { prevRecording = -1 // no previous status } + // CUB-230: Deduplication check - skip if same (camera_id, recorded_at) exists + // This handles replayed entries from offline buffering + var dupCount int + err = s.db.QueryRow(` + SELECT COUNT(*) FROM status_logs + WHERE camera_id = ? AND recorded_at = ? + `, cameraID, ts).Scan(&dupCount) + if err != nil { + log.Printf("MQTT status dedup check error for %s: %v", cameraID, err) + // Continue anyway if check fails + } else if dupCount > 0 { + log.Printf("MQTT status deduplicated (camera_id=%s, recorded_at=%s) - replay from offline buffer", cameraID, ts.Format("2006-01-02 15:04:05")) + return + } + // Insert status_log _, err = s.db.Exec(` INSERT INTO status_logs (camera_id, recorded_at, battery_pct,