OctoPrint-BitBang 0.2.6__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8/OctoPrint_BitBang.egg-info}/PKG-INFO +11 -6
  2. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/OctoPrint_BitBang.egg-info/SOURCES.txt +2 -1
  3. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/OctoPrint_BitBang.egg-info/requires.txt +0 -3
  4. {octoprint_bitbang-0.2.6/OctoPrint_BitBang.egg-info → octoprint_bitbang-0.2.8}/PKG-INFO +11 -6
  5. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/README.md +10 -2
  6. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/__init__.py +8 -1
  7. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/_plugin.py +155 -2
  8. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/bin/bitbang-linux-amd64 +0 -0
  9. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/bin/bitbang-linux-arm64 +0 -0
  10. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/bin/bitbang-linux-armv7 +0 -0
  11. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/static/js/bitbang.js +130 -0
  12. octoprint_bitbang-0.2.8/octoprint_bitbang/templates/bitbang_navbar.jinja2 +4 -0
  13. octoprint_bitbang-0.2.8/octoprint_bitbang/templates/bitbang_settings.jinja2 +110 -0
  14. octoprint_bitbang-0.2.8/octoprint_bitbang/templates/bitbang_wizard.jinja2 +47 -0
  15. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/pyproject.toml +1 -4
  16. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/setup.py +1 -1
  17. octoprint_bitbang-0.2.6/octoprint_bitbang/templates/bitbang_navbar.jinja2 +0 -29
  18. octoprint_bitbang-0.2.6/octoprint_bitbang/templates/bitbang_settings.jinja2 +0 -169
  19. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/LICENSE +0 -0
  20. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/MANIFEST.in +0 -0
  21. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/OctoPrint_BitBang.egg-info/dependency_links.txt +0 -0
  22. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/OctoPrint_BitBang.egg-info/entry_points.txt +0 -0
  23. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/OctoPrint_BitBang.egg-info/top_level.txt +0 -0
  24. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/__main__.py +0 -0
  25. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/app.py +0 -0
  26. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/camera.py +0 -0
  27. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/flip_track.py +0 -0
  28. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/index.html +0 -0
  29. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/octoprint_adapter.py +0 -0
  30. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/pi_camera_track.py +0 -0
  31. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/pi_h264_source.py +0 -0
  32. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/static/favicon.png +0 -0
  33. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/templates/bitbang_webcam.jinja2 +0 -0
  34. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/usb_camera_source.py +0 -0
  35. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/octoprint_bitbang/v4l2_h264_source.py +0 -0
  36. {octoprint_bitbang-0.2.6 → octoprint_bitbang-0.2.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OctoPrint-BitBang
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Remote OctoPrint access with live H.264 video via BitBang WebRTC
5
5
  Author-email: Rich LeGrand <rich.m.legrand@gmail.com>
6
6
  License-Expression: MIT
@@ -24,9 +24,6 @@ Requires-Python: >=3.10
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: bitbang>=0.1.53
27
- Requires-Dist: aiohttp>=3.8.0
28
- Requires-Dist: Pillow>=9.0.0
29
- Requires-Dist: numpy>=1.20.0
30
27
  Requires-Dist: aiortc<1.11
31
28
  Requires-Dist: av<12; platform_machine == "armv7l"
32
29
  Dynamic: license-file
@@ -118,12 +115,20 @@ All settings live in **Settings → BitBang**:
118
115
  | Setting | Effect |
119
116
  |---|---|
120
117
  | Enabled | Toggle BitBang remote access |
121
- | PIN | Optional 4+ digit PIN prompt on the remote URL |
118
+ | PIN | **Required by default.** At least 4 characters; prompted on the remote URL. Remote access stays **off** until a PIN is set (or "Allow no PIN" is ticked). |
119
+ | Allow no PIN | Expose remote access with no PIN (not recommended — anyone with the link can control the printer) |
122
120
  | Camera | Auto-detect, or select from dropdown list |
123
121
  | Resolution | VGA → HD (depending on what selected camera supports) |
124
122
  | Flip horizontal / vertical | Flip video if necessary |
125
123
 
126
- All settings take effect on OctoPrint restart. Full-screen button and brightness slider are overlaid on the video window (Control tab) and update immediately.
124
+ Changes to the PIN and Enabled settings take effect immediately (no OctoPrint restart). Full-screen button and brightness slider are overlaid on the video window (Control tab) and update immediately.
125
+
126
+ ### Set a PIN (required)
127
+
128
+ BitBang exposes a public link to your printer, so it is protected by a PIN. On
129
+ first install a **setup wizard** prompts you to choose one — remote access does
130
+ not start until you do (fail-closed). You can change it any time under
131
+ **Settings → BitBang**.
127
132
 
128
133
  ## How it works
129
134
 
@@ -28,4 +28,5 @@ octoprint_bitbang/static/favicon.png
28
28
  octoprint_bitbang/static/js/bitbang.js
29
29
  octoprint_bitbang/templates/bitbang_navbar.jinja2
30
30
  octoprint_bitbang/templates/bitbang_settings.jinja2
31
- octoprint_bitbang/templates/bitbang_webcam.jinja2
31
+ octoprint_bitbang/templates/bitbang_webcam.jinja2
32
+ octoprint_bitbang/templates/bitbang_wizard.jinja2
@@ -1,7 +1,4 @@
1
1
  bitbang>=0.1.53
2
- aiohttp>=3.8.0
3
- Pillow>=9.0.0
4
- numpy>=1.20.0
5
2
  aiortc<1.11
6
3
 
7
4
  [:platform_machine == "armv7l"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OctoPrint-BitBang
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Remote OctoPrint access with live H.264 video via BitBang WebRTC
5
5
  Author-email: Rich LeGrand <rich.m.legrand@gmail.com>
6
6
  License-Expression: MIT
@@ -24,9 +24,6 @@ Requires-Python: >=3.10
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: bitbang>=0.1.53
27
- Requires-Dist: aiohttp>=3.8.0
28
- Requires-Dist: Pillow>=9.0.0
29
- Requires-Dist: numpy>=1.20.0
30
27
  Requires-Dist: aiortc<1.11
31
28
  Requires-Dist: av<12; platform_machine == "armv7l"
32
29
  Dynamic: license-file
@@ -118,12 +115,20 @@ All settings live in **Settings → BitBang**:
118
115
  | Setting | Effect |
119
116
  |---|---|
120
117
  | Enabled | Toggle BitBang remote access |
121
- | PIN | Optional 4+ digit PIN prompt on the remote URL |
118
+ | PIN | **Required by default.** At least 4 characters; prompted on the remote URL. Remote access stays **off** until a PIN is set (or "Allow no PIN" is ticked). |
119
+ | Allow no PIN | Expose remote access with no PIN (not recommended — anyone with the link can control the printer) |
122
120
  | Camera | Auto-detect, or select from dropdown list |
123
121
  | Resolution | VGA → HD (depending on what selected camera supports) |
124
122
  | Flip horizontal / vertical | Flip video if necessary |
125
123
 
126
- All settings take effect on OctoPrint restart. Full-screen button and brightness slider are overlaid on the video window (Control tab) and update immediately.
124
+ Changes to the PIN and Enabled settings take effect immediately (no OctoPrint restart). Full-screen button and brightness slider are overlaid on the video window (Control tab) and update immediately.
125
+
126
+ ### Set a PIN (required)
127
+
128
+ BitBang exposes a public link to your printer, so it is protected by a PIN. On
129
+ first install a **setup wizard** prompts you to choose one — remote access does
130
+ not start until you do (fail-closed). You can change it any time under
131
+ **Settings → BitBang**.
127
132
 
128
133
  ## How it works
129
134
 
@@ -85,12 +85,20 @@ All settings live in **Settings → BitBang**:
85
85
  | Setting | Effect |
86
86
  |---|---|
87
87
  | Enabled | Toggle BitBang remote access |
88
- | PIN | Optional 4+ digit PIN prompt on the remote URL |
88
+ | PIN | **Required by default.** At least 4 characters; prompted on the remote URL. Remote access stays **off** until a PIN is set (or "Allow no PIN" is ticked). |
89
+ | Allow no PIN | Expose remote access with no PIN (not recommended — anyone with the link can control the printer) |
89
90
  | Camera | Auto-detect, or select from dropdown list |
90
91
  | Resolution | VGA → HD (depending on what selected camera supports) |
91
92
  | Flip horizontal / vertical | Flip video if necessary |
92
93
 
93
- All settings take effect on OctoPrint restart. Full-screen button and brightness slider are overlaid on the video window (Control tab) and update immediately.
94
+ Changes to the PIN and Enabled settings take effect immediately (no OctoPrint restart). Full-screen button and brightness slider are overlaid on the video window (Control tab) and update immediately.
95
+
96
+ ### Set a PIN (required)
97
+
98
+ BitBang exposes a public link to your printer, so it is protected by a PIN. On
99
+ first install a **setup wizard** prompts you to choose one — remote access does
100
+ not start until you do (fail-closed). You can change it any time under
101
+ **Settings → BitBang**.
94
102
 
95
103
  ## How it works
96
104
 
@@ -4,8 +4,15 @@ Remote OctoPrint access with live H.264 video via BitBang WebRTC.
4
4
  No account, no subscription, no port forwarding. One shareable link.
5
5
  """
6
6
 
7
+ from importlib.metadata import PackageNotFoundError, version
8
+
7
9
  __plugin_name__ = "BitBang"
8
- __plugin_version__ = "0.2.6"
10
+ try:
11
+ # Single source of truth: read the version from the installed package
12
+ # metadata (declared once in pyproject.toml) rather than duplicating it here.
13
+ __plugin_version__ = version("OctoPrint-BitBang")
14
+ except PackageNotFoundError: # running from a source checkout, not pip-installed
15
+ __plugin_version__ = "0.0.0+unknown"
9
16
  __plugin_description__ = "Remote OctoPrint access with live H.264 video via BitBang WebRTC. No account, no port forwarding, one shareable link."
10
17
  __plugin_url__ = "https://github.com/richlegrand/OctoPrint-BitBang"
11
18
  __plugin_author__ = "Rich LeGrand"
@@ -47,6 +47,12 @@ except ImportError as e:
47
47
  )
48
48
 
49
49
 
50
+ # A PIN must be empty (remote access then gated/off) or at least this many
51
+ # characters. Mirrored by the wizard/settings JS; enforced server-side in
52
+ # on_settings_save as the authoritative backstop.
53
+ MIN_PIN_LENGTH = 4
54
+
55
+
50
56
  class BitBangPlugin(
51
57
  octoprint.plugin.StartupPlugin,
52
58
  octoprint.plugin.ShutdownPlugin,
@@ -55,12 +61,14 @@ class BitBangPlugin(
55
61
  octoprint.plugin.AssetPlugin,
56
62
  octoprint.plugin.BlueprintPlugin,
57
63
  octoprint.plugin.WebcamProviderPlugin,
64
+ octoprint.plugin.WizardPlugin,
58
65
  ):
59
66
  def __init__(self):
60
67
  super().__init__()
61
68
  self._adapter = None
62
69
  self._thread = None
63
70
  self._local_pcs = set() # track local WebRTC peer connections
71
+ self._running = False # whether the remote-access proxy is live
64
72
 
65
73
  def on_shutdown(self):
66
74
  # Release the camera cleanly when OctoPrint shuts down via its own
@@ -93,8 +101,30 @@ class BitBangPlugin(
93
101
  if not self._settings.get_boolean(["enabled"]):
94
102
  self._logger.info("BitBang disabled in settings")
95
103
  return
104
+ if not self._remote_access_allowed():
105
+ self._logger.warning(
106
+ "BitBang: remote access NOT started — no PIN is set. Set a PIN "
107
+ "in the BitBang settings (or, advanced, explicitly allow running "
108
+ "without one) to enable remote access."
109
+ )
110
+ return
96
111
  self._start_bitbang()
97
112
 
113
+ def _remote_access_allowed(self):
114
+ """Secure-by-default gate. The public tunnel is only exposed when a
115
+ PIN protects it, or the user has explicitly opted into running without
116
+ one. An empty PIN with no opt-in means remote access stays OFF — this
117
+ is the enforcement behind the setup wizard (which merely prompts).
118
+
119
+ Without this, anyone holding the share URL reaches OctoPrint with no
120
+ BitBang-layer auth; for users who enabled OctoPrint's autologinLocal
121
+ that is a full remote takeover (see the X-Forwarded-For handling in
122
+ the Go proxy)."""
123
+ pin = (self._settings.get(["pin"]) or "").strip()
124
+ if pin:
125
+ return True
126
+ return self._settings.get_boolean(["allow_no_pin"])
127
+
98
128
  def _probe_picamera2_sensor(self):
99
129
  # Cache before the adapter opens the camera — picamera2 can't be
100
130
  # opened twice, so the resolutions endpoint relies on this.
@@ -201,11 +231,15 @@ class BitBangPlugin(
201
231
  self._settings.set(["url"], url)
202
232
  self._settings.save()
203
233
  self._logger.info(f"BitBang remote access: {url}")
234
+ # Push the URL to the frontend so the navbar reflects it live, instead
235
+ # of the browser polling the settings API.
236
+ self._plugin_manager.send_plugin_message(self._identifier, {"url": url})
204
237
 
205
238
  # Split-transport: a supervised Go proxy (data + HTTP/WS over native-SCTP
206
239
  # pion) owns the transport under our shared identity, and our camera
207
240
  # track is fed into a video PeerConnection over a socketpair relay.
208
241
  self._start_video_bridge()
242
+ self._running = True
209
243
 
210
244
  def _run_aiortc_loop(self):
211
245
  """Bare asyncio loop for aiortc. Replaces the adapter's own bitbang
@@ -331,6 +365,39 @@ class BitBangPlugin(
331
365
  time.sleep(backoff)
332
366
  backoff = min(backoff * 2, 30)
333
367
 
368
+ def _stop_bitbang(self):
369
+ """Tear down the remote-access proxy (Go subprocess supervisor +
370
+ aiortc loop + camera). Best-effort; each step is independently
371
+ guarded so a failure in one doesn't strand the others."""
372
+ self._running = False
373
+ # Tell the supervisor not to respawn, then kill the current proxy.
374
+ self._go_stop = True
375
+ proc = getattr(self, "_go_proc", None)
376
+ if proc is not None and proc.poll() is None:
377
+ try:
378
+ proc.terminate()
379
+ except Exception:
380
+ pass
381
+ # Release the camera.
382
+ try:
383
+ player = getattr(self._adapter, "player", None) if self._adapter else None
384
+ if player is not None:
385
+ player.stop()
386
+ except Exception as e:
387
+ self._logger.warning(f"BitBang: error stopping camera: {e}")
388
+ # Stop the aiortc event loop so its thread can exit.
389
+ try:
390
+ loop = getattr(self._adapter, "_loop", None) if self._adapter else None
391
+ if loop is not None and loop.is_running():
392
+ loop.call_soon_threadsafe(loop.stop)
393
+ except Exception:
394
+ pass
395
+ self._adapter = None
396
+ # Clear the persisted URL so the navbar doesn't show a dead link.
397
+ self._settings.set(["url"], "")
398
+ self._settings.save()
399
+ self._plugin_manager.send_plugin_message(self._identifier, {"url": ""})
400
+
334
401
  # -- Local WebRTC video signaling --
335
402
 
336
403
  @octoprint.plugin.BlueprintPlugin.route("/ice-servers", methods=["GET"])
@@ -753,6 +820,9 @@ class BitBangPlugin(
753
820
  return {
754
821
  "enabled": True,
755
822
  "pin": "",
823
+ # Explicit opt-in to expose remote access with NO PIN. Default
824
+ # False so a fresh install is gated until the wizard runs.
825
+ "allow_no_pin": False,
756
826
  "url": "",
757
827
  "camera_device": "",
758
828
  "camera_resolution": "640x480",
@@ -762,16 +832,99 @@ class BitBangPlugin(
762
832
  "signaling_server": "bitba.ng",
763
833
  }
764
834
 
835
+ def on_settings_save(self, data):
836
+ # Server-side PIN-policy backstop. The wizard/settings JS validates
837
+ # too, but this is authoritative: a non-empty PIN below the minimum
838
+ # length is rejected (the prior value is kept) rather than persisted.
839
+ if data.get("pin"):
840
+ pin = str(data["pin"]).strip()
841
+ if 0 < len(pin) < MIN_PIN_LENGTH:
842
+ self._logger.warning(
843
+ "BitBang: rejected PIN shorter than %d characters",
844
+ MIN_PIN_LENGTH,
845
+ )
846
+ data["pin"] = self._settings.get(["pin"])
847
+ else:
848
+ data["pin"] = pin
849
+
850
+ # Snapshot gate-relevant state, save, then reconcile if it changed so
851
+ # the wizard's "set a PIN" takes effect without an OctoPrint restart.
852
+ before = self._gate_state()
853
+ octoprint.plugin.SettingsPlugin.on_settings_save(self, data)
854
+ if self._gate_state() != before:
855
+ self._reconcile_remote_access()
856
+
857
+ def _gate_state(self):
858
+ """The tuple of settings that determines whether/how the proxy runs."""
859
+ return (
860
+ self._settings.get_boolean(["enabled"]),
861
+ (self._settings.get(["pin"]) or "").strip(),
862
+ self._settings.get_boolean(["allow_no_pin"]),
863
+ )
864
+
865
+ def _reconcile_remote_access(self):
866
+ """Bring the running proxy in line with current settings. Best-effort
867
+ and defensive: on any failure the user can still restart OctoPrint to
868
+ pick up the change (the prior behavior)."""
869
+ if _VIDEO_IMPORT_ERROR:
870
+ return # video stack unavailable; nothing to start/stop
871
+ try:
872
+ should_run = self._settings.get_boolean(["enabled"]) and self._remote_access_allowed()
873
+ if should_run and not self._running:
874
+ self._logger.info("BitBang: remote access now permitted — starting")
875
+ self._start_bitbang()
876
+ elif not should_run and self._running:
877
+ self._logger.info("BitBang: remote access no longer permitted — stopping")
878
+ self._stop_bitbang()
879
+ elif should_run and self._running:
880
+ # Still running, but the PIN may have changed. The Go proxy
881
+ # reads -pin fresh on each spawn, so bouncing the subprocess
882
+ # (the supervisor respawns it) applies the new PIN.
883
+ self._logger.info("BitBang: PIN/config changed — restarting proxy")
884
+ self._restart_go_proxy()
885
+ except Exception as e:
886
+ self._logger.warning(
887
+ "BitBang: live reconcile failed (%s); restart OctoPrint to apply", e
888
+ )
889
+
890
+ def _restart_go_proxy(self):
891
+ """Bounce just the Go subprocess; the supervisor loop respawns it with
892
+ the current -pin. Falls back to a full restart if no proc is tracked."""
893
+ proc = getattr(self, "_go_proc", None)
894
+ if proc is not None and proc.poll() is None:
895
+ proc.terminate()
896
+ else:
897
+ self._stop_bitbang()
898
+ if self._settings.get_boolean(["enabled"]) and self._remote_access_allowed():
899
+ self._start_bitbang()
900
+
765
901
  def get_template_configs(self):
766
902
  return [
767
- {"type": "settings", "custom_bindings": False},
768
- {"type": "navbar", "custom_bindings": False},
903
+ {"type": "settings", "custom_bindings": True},
904
+ {"type": "navbar", "custom_bindings": True},
905
+ {"type": "wizard", "name": "BitBang", "template": "bitbang_wizard.jinja2"},
769
906
  # Render the live view as a proper webcam provider template so
770
907
  # OctoPrint shows it only when "BitBang Camera" is the selected
771
908
  # webcam -- no DOM-replacing the classic webcam.
772
909
  {"type": "webcam", "name": "BitBang Camera", "template": "bitbang_webcam.jinja2"},
773
910
  ]
774
911
 
912
+ # -- Setup wizard (secure-by-default PIN prompt) --
913
+
914
+ def is_wizard_required(self):
915
+ # Show the wizard whenever remote access would be enabled but is
916
+ # currently ungated (no PIN, no explicit opt-out). Covers fresh
917
+ # installs and existing no-PIN users on upgrade.
918
+ return self._settings.get_boolean(["enabled"]) and not self._remote_access_allowed()
919
+
920
+ def get_wizard_version(self):
921
+ # Bump to re-show the wizard to users who already cleared an older
922
+ # version (e.g. if the PIN policy changes again).
923
+ return 1
924
+
925
+ def get_wizard_details(self):
926
+ return {"required": self.is_wizard_required()}
927
+
775
928
  def get_template_vars(self):
776
929
  return {"plugin_version": __plugin_version__}
777
930
 
@@ -321,3 +321,133 @@
321
321
  }
322
322
  });
323
323
  })();
324
+
325
+ /*
326
+ * Settings + navbar viewmodel.
327
+ *
328
+ * The navbar link and the camera/resolution dropdowns used to read plugin
329
+ * state via raw fetch('/api/settings') + manual DOM. This binds them through
330
+ * the standard settingsViewModel and Knockout instead: settings come from the
331
+ * settings observable, camera lists from the plugin's blueprint endpoints, and
332
+ * the live remote URL arrives via a plugin message (no polling).
333
+ */
334
+ function BitBangViewModel(parameters) {
335
+ var self = this;
336
+ self.settings = parameters[0];
337
+
338
+ self.cameras = ko.observableArray([]);
339
+ self.resolutions = ko.observableArray([]);
340
+ self.remoteUrl = ko.observable("");
341
+
342
+ function bb() { return self.settings.settings.plugins.bitbang; }
343
+
344
+ self.loadResolutions = function (device) {
345
+ return OctoPrint.get(
346
+ "plugin/bitbang/resolutions?device=" + encodeURIComponent(device || "")
347
+ ).done(function (resolutions) {
348
+ self.resolutions(resolutions);
349
+ // Keep the saved resolution if it's still valid for this device,
350
+ // otherwise fall back to the first available.
351
+ if (resolutions.indexOf(bb().camera_resolution()) < 0) {
352
+ bb().camera_resolution(resolutions[0] || "");
353
+ }
354
+ });
355
+ };
356
+
357
+ self.loadCameras = function () {
358
+ return OctoPrint.get("plugin/bitbang/cameras").done(function (cameras) {
359
+ // Lead with an explicit auto-detect entry (value "") so the saved
360
+ // empty setting round-trips cleanly.
361
+ self.cameras([{ device: "", name: "Auto-detect" }].concat(cameras));
362
+ self.loadResolutions(bb().camera_device());
363
+ });
364
+ };
365
+
366
+ self.refreshCameras = function () { self.loadCameras(); };
367
+
368
+ self.showUrl = function () {
369
+ var url = self.remoteUrl();
370
+ if (url) {
371
+ window.prompt("BitBang URL (Ctrl+C to copy):", url);
372
+ } else {
373
+ alert("BitBang URL not available yet");
374
+ }
375
+ };
376
+
377
+ // Repopulate the dropdowns each time the settings dialog opens.
378
+ self.onSettingsShown = function () { self.loadCameras(); };
379
+
380
+ self.onStartupComplete = function () {
381
+ self.remoteUrl(bb().url() || "");
382
+ // Reload resolutions whenever the selected camera changes.
383
+ bb().camera_device.subscribe(function (device) {
384
+ self.loadResolutions(device);
385
+ });
386
+ };
387
+
388
+ // The plugin pushes the remote URL when BitBang connects, so the navbar
389
+ // reflects it live.
390
+ self.onDataUpdaterPluginMessage = function (plugin, data) {
391
+ if (plugin === "bitbang" && data && data.url !== undefined) {
392
+ self.remoteUrl(data.url);
393
+ }
394
+ };
395
+ }
396
+
397
+ OCTOPRINT_VIEWMODELS.push({
398
+ construct: BitBangViewModel,
399
+ dependencies: ["settingsViewModel"],
400
+ elements: ["#navbar_plugin_bitbang", "#settings_plugin_bitbang"]
401
+ });
402
+
403
+ /*
404
+ * Setup-wizard viewmodel. The wizard fields bind directly to the shared
405
+ * settings observables, so OctoPrint persists them on Finish (which then
406
+ * runs the server-side PIN-length backstop in on_settings_save). This VM
407
+ * only supplies live validation feedback for the PIN field.
408
+ */
409
+ function BitBangWizardViewModel(parameters) {
410
+ var self = this;
411
+ self.settings = parameters[0];
412
+
413
+ function bb() {
414
+ var plugins = self.settings.settings.plugins;
415
+ return plugins && plugins.bitbang ? plugins.bitbang : null;
416
+ }
417
+
418
+ function pinOk() {
419
+ var b = bb();
420
+ if (!b) return true;
421
+ return b.allow_no_pin() || (b.pin() || "").trim().length >= 4;
422
+ }
423
+
424
+ self.pinTooShort = ko.pureComputed(function () {
425
+ var b = bb();
426
+ if (!b) return false;
427
+ var pin = (b.pin() || "").trim();
428
+ return pin.length > 0 && pin.length < 4;
429
+ });
430
+
431
+ // Set once the user tries to finish without a usable PIN. Drives the
432
+ // blocking message; clears reactively once they set a valid PIN or
433
+ // tick the no-PIN opt-out.
434
+ self.finishAttempted = ko.observable(false);
435
+ self.finishBlocked = ko.pureComputed(function () {
436
+ return self.finishAttempted() && !pinOk();
437
+ });
438
+
439
+ // Veto the wizard's Finish button unless a valid PIN is set (or the user
440
+ // explicitly opted out). Returning false keeps the dialog open — see
441
+ // OctoPrint wizard.js onBeforeWizardFinish.
442
+ self.onBeforeWizardFinish = function () {
443
+ if (pinOk()) return true;
444
+ self.finishAttempted(true);
445
+ return false;
446
+ };
447
+ }
448
+
449
+ OCTOPRINT_VIEWMODELS.push({
450
+ construct: BitBangWizardViewModel,
451
+ dependencies: ["settingsViewModel"],
452
+ elements: ["#wizard_plugin_bitbang"]
453
+ });
@@ -0,0 +1,4 @@
1
+ <a href="javascript:void(0)" title="BitBang remote access"
2
+ data-bind="visible: settings.settings.plugins.bitbang.enabled, click: showUrl">
3
+ <i class="fas fa-link"></i> <span class="hidden-phone">BitBang</span>
4
+ </a>
@@ -0,0 +1,110 @@
1
+ <div id="settings_bitbang">
2
+ <h3>BitBang Remote Access</h3>
3
+
4
+ <form class="form-horizontal">
5
+ <div class="control-group">
6
+ <label class="control-label">Enabled</label>
7
+ <div class="controls">
8
+ <input type="checkbox" data-bind="checked: settings.settings.plugins.bitbang.enabled">
9
+ </div>
10
+ </div>
11
+
12
+ <div class="control-group">
13
+ <label class="control-label">PIN Protection</label>
14
+ <div class="controls">
15
+ <input type="text" class="input-medium"
16
+ data-bind="value: settings.settings.plugins.bitbang.pin"
17
+ placeholder="At least 4 characters">
18
+ <span class="help-inline">
19
+ Protects remote access. At least 4 characters. Leave empty
20
+ only if you tick &ldquo;allow no PIN&rdquo; below &mdash;
21
+ otherwise remote access stays disabled.
22
+ </span>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="control-group">
27
+ <div class="controls">
28
+ <label class="checkbox">
29
+ <input type="checkbox"
30
+ data-bind="checked: settings.settings.plugins.bitbang.allow_no_pin">
31
+ Allow remote access without a PIN (not recommended)
32
+ </label>
33
+ <span class="help-block muted">
34
+ Anyone who has the share link could control this printer.
35
+ </span>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="control-group">
40
+ <label class="control-label">Remote URL</label>
41
+ <div class="controls">
42
+ <span data-bind="visible: settings.settings.plugins.bitbang.url()">
43
+ <a data-bind="attr: {href: settings.settings.plugins.bitbang.url()}, text: settings.settings.plugins.bitbang.url()"
44
+ target="_blank" rel="noopener"></a>
45
+ </span>
46
+ <span data-bind="visible: !settings.settings.plugins.bitbang.url()" class="muted">
47
+ URL will appear after BitBang connects
48
+ </span>
49
+ <span class="help-inline">Share this link for remote access</span>
50
+ </div>
51
+ </div>
52
+
53
+ <h3>Camera</h3>
54
+
55
+ <div class="control-group">
56
+ <label class="control-label">Camera</label>
57
+ <div class="controls">
58
+ <select class="input-xlarge"
59
+ data-bind="options: cameras,
60
+ optionsText: function(c) { return c.device ? c.name + ' (' + c.device + ')' : c.name; },
61
+ optionsValue: 'device',
62
+ value: settings.settings.plugins.bitbang.camera_device,
63
+ valueAllowUnset: true"></select>
64
+ <button class="btn btn-mini" type="button" data-bind="click: refreshCameras" title="Refresh camera list">
65
+ <i class="fas fa-sync"></i>
66
+ </button>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="control-group">
71
+ <label class="control-label">Resolution</label>
72
+ <div class="controls">
73
+ <select class="input-medium"
74
+ data-bind="options: resolutions,
75
+ value: settings.settings.plugins.bitbang.camera_resolution,
76
+ valueAllowUnset: true"></select>
77
+ </div>
78
+ </div>
79
+
80
+ <div class="control-group">
81
+ <label class="control-label">Image orientation</label>
82
+ <div class="controls">
83
+ <label class="checkbox inline">
84
+ <input type="checkbox" data-bind="checked: settings.settings.plugins.bitbang.flip_horizontal">
85
+ Flip horizontal
86
+ </label>
87
+ <label class="checkbox inline">
88
+ <input type="checkbox" data-bind="checked: settings.settings.plugins.bitbang.flip_vertical">
89
+ Flip vertical
90
+ </label>
91
+ </div>
92
+ </div>
93
+
94
+ <h3>Advanced</h3>
95
+
96
+ <div class="control-group">
97
+ <label class="control-label">Signaling server</label>
98
+ <div class="controls">
99
+ <input type="text" class="input-xlarge"
100
+ data-bind="value: settings.settings.plugins.bitbang.signaling_server">
101
+ <span class="help-inline">Default <code>bitba.ng</code>. To use your own <a href="https://github.com/richlegrand/bitbang-server" target="_blank" rel="noopener">bitbang-server</a>, replace with the hostname (e.g. <code>signaling.example.com</code>).</span>
102
+ </div>
103
+ </div>
104
+ </form>
105
+
106
+ <div class="alert alert-info" style="margin-top: 16px;">
107
+ <i class="fas fa-info-circle"></i>
108
+ <strong>Restart OctoPrint</strong> after changing settings for them to take effect.
109
+ </div>
110
+ </div>
@@ -0,0 +1,47 @@
1
+ <div id="wizard_plugin_bitbang">
2
+ <h3>Secure your BitBang remote access</h3>
3
+ <p>
4
+ BitBang gives this OctoPrint a private link you can use to reach it from
5
+ anywhere. Protect that link with a PIN so only you can connect &mdash; we
6
+ recommend at least {{ 4 }} characters.
7
+ </p>
8
+
9
+ <form class="form-horizontal">
10
+ <div class="control-group">
11
+ <label class="control-label">PIN</label>
12
+ <div class="controls">
13
+ <input type="text" class="input-medium"
14
+ data-bind="value: settings.settings.plugins.bitbang.pin, valueUpdate: 'afterkeydown'"
15
+ placeholder="At least 4 characters">
16
+ <span class="help-inline" style="color: #b94a48;"
17
+ data-bind="visible: pinTooShort">
18
+ PIN must be at least 4 characters.
19
+ </span>
20
+ </div>
21
+ </div>
22
+
23
+ <div class="control-group">
24
+ <div class="controls">
25
+ <label class="checkbox">
26
+ <input type="checkbox"
27
+ data-bind="checked: settings.settings.plugins.bitbang.allow_no_pin">
28
+ Advanced: run without a PIN
29
+ </label>
30
+ <span class="help-block muted">
31
+ Not recommended &mdash; anyone who has the link could control
32
+ this printer.
33
+ </span>
34
+ </div>
35
+ </div>
36
+ </form>
37
+
38
+ <p class="muted">
39
+ Until you set a PIN (or tick the box above), BitBang remote access stays
40
+ disabled.
41
+ </p>
42
+
43
+ <div class="alert alert-error" data-bind="visible: finishBlocked" style="display: none;">
44
+ Set a PIN of at least 4 characters, or tick &ldquo;run without a
45
+ PIN&rdquo; above, before finishing.
46
+ </div>
47
+ </div>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "OctoPrint-BitBang"
7
- version = "0.2.6"
7
+ version = "0.2.8"
8
8
  description = "Remote OctoPrint access with live H.264 video via BitBang WebRTC"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -27,9 +27,6 @@ classifiers = [
27
27
  ]
28
28
  dependencies = [
29
29
  "bitbang>=0.1.53",
30
- "aiohttp>=3.8.0",
31
- "Pillow>=9.0.0",
32
- "numpy>=1.20.0",
33
30
  # aiortc/av come transitively via bitbang. aiortc<1.11 already caps av<14.
34
31
  # 32-bit (armv7l) has NO PyAV wheels at any in-range version, so av builds
35
32
  # from source against the system FFmpeg (Bookworm 5.1); av<12 lands it on the
@@ -1,2 +1,2 @@
1
1
  from setuptools import setup
2
- setup()
2
+ setup(license="MIT")
@@ -1,29 +0,0 @@
1
- <a id="bitbang_navbar_link" href="javascript:void(0)" title="BitBang remote access" style="display:none" onclick="
2
- fetch('/api/settings')
3
- .then(function(r) { return r.json(); })
4
- .then(function(data) {
5
- var url = data.plugins && data.plugins.bitbang && data.plugins.bitbang.url;
6
- if (url) {
7
- prompt('BitBang URL (Ctrl+C to copy):', url);
8
- } else {
9
- alert('BitBang URL not available yet');
10
- }
11
- })
12
- .catch(function() { alert('Could not load settings'); });
13
- ">
14
- <i id="bitbang_navbar_icon" class="fas fa-link"></i> <span class="hidden-phone">BitBang</span>
15
- </a>
16
- <script>
17
- (function () {
18
- fetch('/api/settings')
19
- .then(function (r) { return r.json(); })
20
- .then(function (data) {
21
- var enabled = data.plugins && data.plugins.bitbang && data.plugins.bitbang.enabled;
22
- if (enabled) {
23
- var link = document.getElementById('bitbang_navbar_link');
24
- if (link) link.style.display = '';
25
- }
26
- })
27
- .catch(function () {});
28
- })();
29
- </script>
@@ -1,169 +0,0 @@
1
- <div id="settings_bitbang">
2
- <h3>BitBang Remote Access</h3>
3
-
4
- <form class="form-horizontal">
5
- <div class="control-group">
6
- <label class="control-label">Enabled</label>
7
- <div class="controls">
8
- <input type="checkbox" data-bind="checked: settings.plugins.bitbang.enabled">
9
- </div>
10
- </div>
11
-
12
- <div class="control-group">
13
- <label class="control-label">PIN Protection</label>
14
- <div class="controls">
15
- <input type="text" class="input-medium"
16
- data-bind="value: settings.plugins.bitbang.pin"
17
- placeholder="Empty to disable">
18
- <span class="help-inline">Optional PIN to protect remote access</span>
19
- </div>
20
- </div>
21
-
22
- <div class="control-group">
23
- <label class="control-label">Remote URL</label>
24
- <div class="controls">
25
- <span data-bind="visible: settings.plugins.bitbang.url()">
26
- <a data-bind="attr: {href: settings.plugins.bitbang.url()}, text: settings.plugins.bitbang.url()"
27
- target="_blank" rel="noopener"></a>
28
- </span>
29
- <span data-bind="visible: !settings.plugins.bitbang.url()" class="muted">
30
- URL will appear after BitBang connects
31
- </span>
32
- <span class="help-inline">Share this link for remote access</span>
33
- </div>
34
- </div>
35
-
36
- <h3>Camera</h3>
37
-
38
- <div class="control-group">
39
- <label class="control-label">Camera</label>
40
- <div class="controls">
41
- <select id="bitbang_camera_select" class="input-xlarge"
42
- data-bind="value: settings.plugins.bitbang.camera_device, valueAllowUnset: true">
43
- <option value="">Auto-detect</option>
44
- </select>
45
- <button class="btn btn-mini" id="bitbang_refresh_cameras" type="button">
46
- <i class="fas fa-sync"></i>
47
- </button>
48
- </div>
49
- </div>
50
-
51
- <div class="control-group">
52
- <label class="control-label">Resolution</label>
53
- <div class="controls">
54
- <select id="bitbang_resolution_select" class="input-medium"
55
- data-bind="value: settings.plugins.bitbang.camera_resolution, valueAllowUnset: true">
56
- <option value="640x480">640x480</option>
57
- </select>
58
- </div>
59
- </div>
60
-
61
- <div class="control-group">
62
- <label class="control-label">Image orientation</label>
63
- <div class="controls">
64
- <label class="checkbox inline">
65
- <input type="checkbox" data-bind="checked: settings.plugins.bitbang.flip_horizontal">
66
- Flip horizontal
67
- </label>
68
- <label class="checkbox inline">
69
- <input type="checkbox" data-bind="checked: settings.plugins.bitbang.flip_vertical">
70
- Flip vertical
71
- </label>
72
- </div>
73
- </div>
74
-
75
- <h3>Advanced</h3>
76
-
77
- <div class="control-group">
78
- <label class="control-label">Signaling server</label>
79
- <div class="controls">
80
- <input type="text" class="input-xlarge"
81
- data-bind="value: settings.plugins.bitbang.signaling_server">
82
- <span class="help-inline">Default <code>bitba.ng</code>. To use your own <a href="https://github.com/richlegrand/bitbang-server" target="_blank" rel="noopener">bitbang-server</a>, replace with the hostname (e.g. <code>signaling.example.com</code>).</span>
83
- </div>
84
- </div>
85
- </form>
86
-
87
- <div class="alert alert-info" style="margin-top: 16px;">
88
- <i class="fas fa-info-circle"></i>
89
- <strong>Restart OctoPrint</strong> after changing settings for them to take effect.
90
- </div>
91
- </div>
92
-
93
- <script>
94
- (function () {
95
- var cameraSelect = document.getElementById("bitbang_camera_select");
96
- var resolutionSelect = document.getElementById("bitbang_resolution_select");
97
- var refreshBtn = document.getElementById("bitbang_refresh_cameras");
98
- if (!cameraSelect || !resolutionSelect) return;
99
-
100
- // Set a <select> and notify Knockout's value binding — it doesn't see
101
- // options added via raw DOM, so without the dispatched 'change' the
102
- // observable (and thus the saved setting) won't follow the selection.
103
- // Suppress the camera change handler during programmatic sets so we don't
104
- // reload resolutions twice.
105
- function setSelect(sel, value) {
106
- var ok = value && Array.prototype.some.call(
107
- sel.options, function (o) { return o.value === value; });
108
- sel._suppress = true;
109
- if (ok) sel.value = value;
110
- sel.dispatchEvent(new Event("change"));
111
- sel._suppress = false;
112
- }
113
-
114
- function loadResolutions(device, wanted) {
115
- return fetch("/plugin/bitbang/resolutions?device=" + encodeURIComponent(device || ""))
116
- .then(function (r) { return r.json(); })
117
- .then(function (resolutions) {
118
- resolutionSelect.innerHTML = "";
119
- resolutions.forEach(function (res) {
120
- var opt = document.createElement("option");
121
- opt.value = res;
122
- opt.textContent = res;
123
- resolutionSelect.appendChild(opt);
124
- });
125
- setSelect(resolutionSelect,
126
- resolutions.indexOf(wanted) >= 0 ? wanted : resolutions[0]);
127
- })
128
- .catch(function () {});
129
- }
130
-
131
- function loadCameras(wantedCamera, wantedRes) {
132
- return fetch("/plugin/bitbang/cameras")
133
- .then(function (r) { return r.json(); })
134
- .then(function (cameras) {
135
- while (cameraSelect.options.length > 1) cameraSelect.remove(1);
136
- cameras.forEach(function (cam) {
137
- var opt = document.createElement("option");
138
- opt.value = cam.device;
139
- opt.textContent = cam.name + " (" + cam.device + ")";
140
- cameraSelect.appendChild(opt);
141
- });
142
- setSelect(cameraSelect, wantedCamera);
143
- return loadResolutions(cameraSelect.value, wantedRes);
144
- })
145
- .catch(function () {});
146
- }
147
-
148
- cameraSelect.addEventListener("change", function () {
149
- if (cameraSelect._suppress) return; // programmatic set — skip reload
150
- loadResolutions(cameraSelect.value, resolutionSelect.value);
151
- });
152
-
153
- if (refreshBtn) {
154
- refreshBtn.addEventListener("click", function () {
155
- loadCameras(cameraSelect.value, resolutionSelect.value);
156
- });
157
- }
158
-
159
- // Populate from the SAVED config — not the (Knockout-clobbered) DOM value —
160
- // so the dropdowns reflect what's persisted and a Save round-trips cleanly.
161
- fetch("/api/settings")
162
- .then(function (r) { return r.json(); })
163
- .then(function (s) {
164
- var bb = (s.plugins && s.plugins.bitbang) || {};
165
- loadCameras(bb.camera_device || "", bb.camera_resolution || "");
166
- })
167
- .catch(function () { loadCameras("", ""); });
168
- })();
169
- </script>