bluer-sbc 8.3.1__py3-none-any.whl

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.

Potentially problematic release.


This version of bluer-sbc might be problematic. Click here for more details.

Files changed (82) hide show
  1. bluer_sbc/.abcli/abcli.sh +12 -0
  2. bluer_sbc/.abcli/actions.sh +11 -0
  3. bluer_sbc/.abcli/adafruit_rgb_matrix.sh +14 -0
  4. bluer_sbc/.abcli/alias.sh +5 -0
  5. bluer_sbc/.abcli/blue_sbc.sh +11 -0
  6. bluer_sbc/.abcli/camera.sh +20 -0
  7. bluer_sbc/.abcli/grove.sh +43 -0
  8. bluer_sbc/.abcli/hat.sh +22 -0
  9. bluer_sbc/.abcli/install/adafruit_rgb_matrix.sh +15 -0
  10. bluer_sbc/.abcli/install/grove.sh +29 -0
  11. bluer_sbc/.abcli/install/lepton.sh +33 -0
  12. bluer_sbc/.abcli/install/rpi.sh +65 -0
  13. bluer_sbc/.abcli/install/scroll_phat_hd.sh +14 -0
  14. bluer_sbc/.abcli/install/sparkfun_top_phat.sh +33 -0
  15. bluer_sbc/.abcli/install/template.sh +9 -0
  16. bluer_sbc/.abcli/install/unicorn_16x16.sh +16 -0
  17. bluer_sbc/.abcli/lepton.sh +15 -0
  18. bluer_sbc/.abcli/scroll_phat_hd.sh +14 -0
  19. bluer_sbc/.abcli/session.sh +39 -0
  20. bluer_sbc/.abcli/sparkfun_top_phat.sh +27 -0
  21. bluer_sbc/.abcli/tests/README.sh +8 -0
  22. bluer_sbc/.abcli/tests/camera.sh +47 -0
  23. bluer_sbc/.abcli/tests/help.sh +65 -0
  24. bluer_sbc/.abcli/tests/version.sh +8 -0
  25. bluer_sbc/.abcli/unicorn_16x16.sh +14 -0
  26. bluer_sbc/README.py +51 -0
  27. bluer_sbc/__init__.py +17 -0
  28. bluer_sbc/__main__.py +16 -0
  29. bluer_sbc/algo/__init__.py +0 -0
  30. bluer_sbc/algo/diff.py +81 -0
  31. bluer_sbc/config.env +30 -0
  32. bluer_sbc/env.py +35 -0
  33. bluer_sbc/hardware/__init__.py +38 -0
  34. bluer_sbc/hardware/adafruit_rgb_matrix.py +30 -0
  35. bluer_sbc/hardware/display.py +112 -0
  36. bluer_sbc/hardware/grove.py +104 -0
  37. bluer_sbc/hardware/hardware.py +58 -0
  38. bluer_sbc/hardware/hat/__init__.py +0 -0
  39. bluer_sbc/hardware/hat/__main__.py +91 -0
  40. bluer_sbc/hardware/hat/abstract.py +136 -0
  41. bluer_sbc/hardware/hat/prototype.py +161 -0
  42. bluer_sbc/hardware/screen.py +17 -0
  43. bluer_sbc/hardware/scroll_phat_hd.py +35 -0
  44. bluer_sbc/hardware/sparkfun_top_phat/__init__.py +0 -0
  45. bluer_sbc/hardware/sparkfun_top_phat/__main__.py +51 -0
  46. bluer_sbc/hardware/sparkfun_top_phat/classes.py +104 -0
  47. bluer_sbc/hardware/unicorn_16x16.py +44 -0
  48. bluer_sbc/help/__init__.py +0 -0
  49. bluer_sbc/help/__main__.py +10 -0
  50. bluer_sbc/help/adafruit_rgb_matrix.py +23 -0
  51. bluer_sbc/help/camera.py +71 -0
  52. bluer_sbc/help/functions.py +52 -0
  53. bluer_sbc/help/grove.py +59 -0
  54. bluer_sbc/help/hat.py +56 -0
  55. bluer_sbc/help/lepton.py +39 -0
  56. bluer_sbc/help/scroll_phat_hd.py +23 -0
  57. bluer_sbc/help/session.py +26 -0
  58. bluer_sbc/help/sparkfun_top_phat.py +26 -0
  59. bluer_sbc/help/unicorn_16x16.py +23 -0
  60. bluer_sbc/host.py +11 -0
  61. bluer_sbc/imager/__init__.py +16 -0
  62. bluer_sbc/imager/camera/__init__.py +3 -0
  63. bluer_sbc/imager/camera/__main__.py +69 -0
  64. bluer_sbc/imager/camera/classes.py +259 -0
  65. bluer_sbc/imager/camera/constants.py +30 -0
  66. bluer_sbc/imager/classes.py +25 -0
  67. bluer_sbc/imager/lepton/__init__.py +3 -0
  68. bluer_sbc/imager/lepton/__main__.py +51 -0
  69. bluer_sbc/imager/lepton/classes.py +35 -0
  70. bluer_sbc/imager/lepton/python2.py +70 -0
  71. bluer_sbc/logger.py +5 -0
  72. bluer_sbc/sample.env +1 -0
  73. bluer_sbc/session/__init__.py +0 -0
  74. bluer_sbc/session/__main__.py +27 -0
  75. bluer_sbc/session/classes.py +318 -0
  76. bluer_sbc/session/functions.py +22 -0
  77. bluer_sbc/urls.py +1 -0
  78. bluer_sbc-8.3.1.dist-info/METADATA +58 -0
  79. bluer_sbc-8.3.1.dist-info/RECORD +82 -0
  80. bluer_sbc-8.3.1.dist-info/WHEEL +5 -0
  81. bluer_sbc-8.3.1.dist-info/licenses/LICENSE +121 -0
  82. bluer_sbc-8.3.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,25 @@
1
+ from typing import Tuple, List
2
+ import numpy as np
3
+
4
+ from blueness import module
5
+
6
+ from bluer_sbc import NAME
7
+
8
+ NAME = module.name(__file__, NAME)
9
+
10
+
11
+ class Imager:
12
+ def capture(self) -> Tuple[bool, np.ndarray]:
13
+ return True, np.zeros(())
14
+
15
+
16
+ class TemplateImager(Imager):
17
+ def capture(self) -> Tuple[bool, np.ndarray]:
18
+ success, image = super().capture()
19
+
20
+ # TODO: capture the image here
21
+
22
+ return success, image
23
+
24
+ def signature(self) -> List[str]:
25
+ return [self.__class__.__name__]
@@ -0,0 +1,3 @@
1
+ from bluer_sbc.imager.lepton.classes import Lepton
2
+
3
+ instance = Lepton()
@@ -0,0 +1,51 @@
1
+ import argparse
2
+ import os
3
+
4
+ from blueness import module
5
+
6
+ from bluer_sbc import NAME
7
+ from bluer_sbc.imager.lepton import instance as lepton
8
+ from bluer_sbc.hardware import hardware
9
+ from bluer_sbc.logger import logger
10
+
11
+ NAME = module.name(__file__, NAME)
12
+
13
+ parser = argparse.ArgumentParser(NAME)
14
+ parser.add_argument(
15
+ "task",
16
+ type=str,
17
+ default="",
18
+ help="capture|preview",
19
+ )
20
+ parser.add_argument(
21
+ "--filename",
22
+ default="",
23
+ type=str,
24
+ )
25
+ parser.add_argument(
26
+ "--output_path",
27
+ type=str,
28
+ default="",
29
+ )
30
+ args = parser.parse_args()
31
+
32
+ success = False
33
+ if args.task == "capture":
34
+ success, _, _ = lepton.capture(
35
+ filename=os.path.join(args.output_path, "camera.jpg"),
36
+ )
37
+ elif args.task == "preview":
38
+ success = True
39
+
40
+ hardware.sign_images = False
41
+ try:
42
+ while not hardware.pressed("qe"):
43
+ _, image = lepton.capture()
44
+ hardware.update_screen(image)
45
+ finally:
46
+ pass
47
+ else:
48
+ logger.error(f"-{NAME}: {args.task}: command not found.")
49
+
50
+ if not success:
51
+ logger.error(f"-{NAME}: {args.task}: failed.")
@@ -0,0 +1,35 @@
1
+ from typing import Tuple
2
+ import numpy as np
3
+
4
+ from blueness import module
5
+ from bluer_options import string
6
+ from bluer_objects import file
7
+ from bluer_objects import path
8
+ from bluer_objects.env import abcli_path_git
9
+ from bluer_objects.host import shell
10
+
11
+ from bluer_sbc import NAME
12
+ from bluer_sbc.imager.classes import Imager
13
+ from bluer_sbc.logger import logger
14
+
15
+
16
+ NAME = module.name(__file__, NAME)
17
+
18
+
19
+ class Lepton(Imager):
20
+ def capture(self) -> Tuple[bool, np.ndarray]:
21
+ success, image = super().capture()
22
+
23
+ temp_dir = path.auxiliary("lepton")
24
+ success = shell(
25
+ f"python python2.py capture --output_path {temp_dir}",
26
+ work_dir=f"{abcli_path_git}/bluer-sbc/bluer_sbc/imager/lepton",
27
+ )
28
+
29
+ if success:
30
+ success, image = file.load_image(f"{temp_dir}/image.jpg")
31
+
32
+ if success:
33
+ logger.info(f"{NAME}.capture(): {string.pretty_shape_of_matrix(image)}")
34
+
35
+ return success, image
@@ -0,0 +1,70 @@
1
+ import argparse
2
+ import cv2
3
+ from datetime import datetime
4
+ import numpy as np
5
+ import os.path
6
+ import time
7
+
8
+
9
+ def capture(flip_v=True, device="/dev/spidev0.0"):
10
+ from pylepton import Lepton
11
+
12
+ with Lepton(device) as l:
13
+ a, _ = l.capture()
14
+
15
+ if flip_v:
16
+ cv2.flip(a, 0, a)
17
+
18
+ cv2.normalize(a, a, 0, 65535, cv2.NORM_MINMAX)
19
+ np.right_shift(a, 8, a)
20
+
21
+ return np.uint8(a)
22
+
23
+
24
+ def capture_and_save(path):
25
+ try:
26
+ for _ in range(2):
27
+ image = capture(False, "/dev/spidev0.0")
28
+ time.sleep(1)
29
+
30
+ cv2.imwrite(os.path.join(path, "image_raw.jpg"), image)
31
+
32
+ cv2.imwrite(
33
+ os.path.join(path, "image.jpg"),
34
+ cv2.resize(image, (1280, 960), interpolation=cv2.INTER_NEAREST),
35
+ )
36
+
37
+ print(
38
+ "lepton.capture_and_save({}) completed: {}.".format(
39
+ path, "x".join([str(dim) for dim in image.shape])
40
+ )
41
+ )
42
+ except Exception as e:
43
+ print("lepton.capture_and_save() crashed: {}".format(e))
44
+ return False
45
+
46
+ return True
47
+
48
+
49
+ if __name__ == "__main__":
50
+ parser = argparse.ArgumentParser()
51
+ parser.add_argument(
52
+ "task",
53
+ type=str,
54
+ default="",
55
+ help="capture",
56
+ )
57
+ parser.add_argument(
58
+ "--output_path",
59
+ default="",
60
+ )
61
+ args = parser.parse_args()
62
+
63
+ success = False
64
+ if args.task == "capture":
65
+ success = capture_and_save(args.output_path)
66
+ else:
67
+ print("-lepton: {}: command not found.".format(args.task))
68
+
69
+ if not success:
70
+ print("-lepton: {}: failed.".format(args.task))
bluer_sbc/logger.py ADDED
@@ -0,0 +1,5 @@
1
+ from bluer_options.logger import get_logger
2
+
3
+ from bluer_sbc import ICON
4
+
5
+ logger = get_logger(ICON)
bluer_sbc/sample.env ADDED
@@ -0,0 +1 @@
1
+ BLUER_SBC_SECRET=value
File without changes
@@ -0,0 +1,27 @@
1
+ import argparse
2
+
3
+ from blueness import module
4
+
5
+ from bluer_sbc import NAME
6
+ from bluer_sbc.session.classes import Session
7
+ from bluer_sbc.logger import logger
8
+
9
+ NAME = module.name(__file__, NAME)
10
+
11
+
12
+ parser = argparse.ArgumentParser(NAME)
13
+ parser.add_argument(
14
+ "task",
15
+ type=str,
16
+ help="start",
17
+ )
18
+ args = parser.parse_args()
19
+
20
+ success = False
21
+ if args.task == "start":
22
+ success = Session.start()
23
+ else:
24
+ logger.error(f"-{NAME}: {args.task}: command not found.")
25
+
26
+ if not success:
27
+ logger.error(f"-{NAME}: {args.task}: failed.")
@@ -0,0 +1,318 @@
1
+ import os
2
+
3
+ from blueness import module
4
+ from bluer_options import string
5
+ from bluer_options import host
6
+ from bluer_options.logger import crash_report
7
+ from bluer_options.timer import Timer
8
+ from bluer_objects import file
9
+ from bluer_objects import objects
10
+ from bluer_objects.storage import instance as storage
11
+ from bluer_objects.graphics.signature import add_signature
12
+ from bluer_ai import VERSION as abcli_VERSION
13
+ from bluer_ai.modules import terraform
14
+ from bluer_objects.env import abcli_object_name
15
+
16
+ from bluer_sbc import NAME
17
+ from bluer_sbc import env
18
+ from bluer_sbc.host import signature
19
+ from bluer_sbc.session.functions import reply_to_bash
20
+ from bluer_sbc.algo.diff import Diff
21
+ from bluer_sbc.hardware import hardware
22
+ from bluer_sbc.imager import imager
23
+ from bluer_sbc.logger import logger
24
+
25
+
26
+ NAME = module.name(__file__, NAME)
27
+
28
+
29
+ class Session:
30
+ def __init__(self):
31
+ self.bash_keys = {
32
+ "e": "exit",
33
+ "r": "reboot",
34
+ "s": "shutdown",
35
+ "u": "update",
36
+ }
37
+
38
+ self.diff = Diff(env.BLUER_SBC_SESSION_IMAGER_DIFF)
39
+
40
+ self.capture_requested = False
41
+
42
+ self.frame = 0
43
+ self.new_frame = False
44
+ self.frame_image = terraform.poster(None)
45
+ self.frame_filename = ""
46
+
47
+ self.auto_upload = env.BLUER_SBC_SESSION_AUTO_UPLOAD
48
+
49
+ self.messages = []
50
+
51
+ self.model = None
52
+
53
+ self.params = {"iteration": -1}
54
+
55
+ self.state = {}
56
+
57
+ self.timer = {}
58
+ for name, period in {
59
+ "imager": env.BLUER_SBC_SESSION_IMAGER_PERIOD,
60
+ "messenger": env.BLUER_SBC_SESSION_MESSENGER_PERIOD,
61
+ "reboot": env.BLUER_SBC_SESSION_REBOOT_PERIOD,
62
+ "screen": env.BLUER_SBC_SESSION_SCREEN_PERIOD,
63
+ "temperature": env.BLUER_SBC_SESSION_TEMPERATURE_PERIOD,
64
+ }.items():
65
+ self.add_timer(name, period)
66
+
67
+ def add_timer(
68
+ self,
69
+ name: str,
70
+ period: float,
71
+ ):
72
+ if name not in self.timer:
73
+ self.timer[name] = Timer(period, name)
74
+ logger.info(
75
+ "{}: timer[{}]:{}".format(
76
+ NAME,
77
+ name,
78
+ string.pretty_frequency(1 / period),
79
+ )
80
+ )
81
+ return True
82
+ return False
83
+
84
+ def check_imager(self):
85
+ self.new_frame = False
86
+
87
+ if not env.BLUER_SBC_SESSION_IMAGER_ENABLED:
88
+ return
89
+ if not self.capture_requested and not self.timer["imager"].tick():
90
+ return
91
+ self.capture_requested = False
92
+
93
+ success, image = imager.capture()
94
+ if not success:
95
+ return
96
+
97
+ hardware.pulse("data")
98
+
99
+ if self.diff.same(image):
100
+ return
101
+
102
+ self.frame += 1
103
+
104
+ image = add_signature(
105
+ image,
106
+ [" | ".join(objects.signature(self.frame))],
107
+ [" | ".join(signature())],
108
+ )
109
+
110
+ filename = objects.path_of(
111
+ object_name=abcli_object_name,
112
+ filename=f"{self.frame:016d}.jpg",
113
+ )
114
+ if not file.save_image(filename, image):
115
+ return
116
+
117
+ self.new_frame = True
118
+ self.frame_image = image
119
+ self.frame_filename = filename
120
+
121
+ if self.auto_upload:
122
+ storage.upload_file(self.frame_filename)
123
+
124
+ def check_keys(self):
125
+ for key in hardware.key_buffer:
126
+ if key in self.bash_keys:
127
+ reply_to_bash(self.bash_keys[key])
128
+ return False
129
+
130
+ if " " in hardware.key_buffer:
131
+ self.capture_requested = True
132
+
133
+ hardware.key_buffer = []
134
+
135
+ return None
136
+
137
+ def check_seed(self):
138
+ seed_filename = host.get_seed_filename()
139
+ if not file.exists(seed_filename):
140
+ return None
141
+
142
+ success, content = file.load_json(file.set_extension(seed_filename, "json"))
143
+ if not success:
144
+ return None
145
+
146
+ hardware.pulse("outputs")
147
+
148
+ seed_version = content.get("version", "")
149
+ if seed_version <= abcli_VERSION:
150
+ return None
151
+
152
+ logger.info(f"{NAME}: seed {seed_version} detected.")
153
+ reply_to_bash("seed", [seed_filename])
154
+ return False
155
+
156
+ def check_timers(self):
157
+ if self.timer["screen"].tick():
158
+ hardware.update_screen(
159
+ image=self.frame_image,
160
+ session=self,
161
+ header=self.signature(),
162
+ )
163
+ elif hardware.animated:
164
+ hardware.animate()
165
+
166
+ if self.timer["reboot"].tick("wait"):
167
+ reply_to_bash("reboot")
168
+ return False
169
+
170
+ if self.timer["temperature"].tick():
171
+ self.read_temperature()
172
+
173
+ return None
174
+
175
+ def close(self):
176
+ hardware.release()
177
+
178
+ def process_message(self, message):
179
+ if (
180
+ env.BLUER_SBC_SESSION_OUTBOUND_QUEUE
181
+ and message.subject in "bolt,frame".split(",")
182
+ and not host.is_headless()
183
+ ):
184
+ logger.info(f"{NAME}: frame received: {message.as_string()}")
185
+ self.new_frame, self.frame_image = file.load_image(message.filename)
186
+
187
+ if message.subject == "capture":
188
+ logger.info(f"{NAME}: capture message received.")
189
+ self.capture_requested = True
190
+
191
+ if message.subject in "reboot,shutdown".split(","):
192
+ logger.info(f"{NAME}: {message.subject} message received.")
193
+ reply_to_bash(message.subject)
194
+ return False
195
+
196
+ if message.subject == "update":
197
+ try:
198
+ if message.data["version"] > abcli_VERSION:
199
+ reply_to_bash("update")
200
+ return False
201
+ except Exception as e:
202
+ crash_report(e)
203
+
204
+ return None
205
+
206
+ # https://www.cyberciti.biz/faq/linux-find-out-raspberry-pi-gpu-and-arm-cpu-temperature-command/
207
+ def read_temperature(self):
208
+ if not host.is_rpi():
209
+ return
210
+
211
+ params = {}
212
+
213
+ success, output = file.load_text("/sys/class/thermal/thermal_zone0/temp")
214
+ if success:
215
+ output = [thing for thing in output if thing]
216
+ if output:
217
+ try:
218
+ params["temperature.cpu"] = float(output[0]) / 1000
219
+ except Exception as e:
220
+ crash_report(e)
221
+ return
222
+
223
+ self.params.update(params)
224
+ logger.info(
225
+ "{}: {}".format(
226
+ NAME,
227
+ ", ".join(string.pretty_param(params)),
228
+ )
229
+ )
230
+
231
+ def signature(self):
232
+ return [
233
+ " | ".join(objects.signature()),
234
+ " | ".join(sorted([timer.signature() for timer in self.timer.values()])),
235
+ " | ".join(
236
+ (["*"] if self.new_frame else [])
237
+ + (["^"] if self.auto_upload else [])
238
+ + ([f">{self.outbound_queue}"] if self.outbound_queue else [])
239
+ + hardware.signature()
240
+ + [
241
+ "diff: {:.03f} - {}".format(
242
+ self.diff.last_diff,
243
+ string.pretty_duration(
244
+ self.diff.last_same_period,
245
+ largest=True,
246
+ include_ms=True,
247
+ short=True,
248
+ ),
249
+ ),
250
+ string.pretty_shape_of_matrix(self.frame_image),
251
+ ]
252
+ + ([] if self.model is None else self.model.signature())
253
+ ),
254
+ ]
255
+
256
+ @staticmethod
257
+ def start():
258
+ success = True
259
+ logger.info(f"{NAME}: started ...")
260
+
261
+ try:
262
+ session = Session()
263
+
264
+ while session.step():
265
+ pass
266
+
267
+ logger.info(f"{NAME}: stopped.")
268
+ except KeyboardInterrupt:
269
+ logger.info(f"{NAME}: Ctrl+C: stopped.")
270
+ reply_to_bash("exit")
271
+ except Exception as e:
272
+ crash_report(e)
273
+ success = False
274
+
275
+ try:
276
+ session.close()
277
+ except Exception as e:
278
+ crash_report(e)
279
+ success = False
280
+
281
+ return success
282
+
283
+ def step(
284
+ self,
285
+ steps="all",
286
+ ) -> bool:
287
+ if steps == "all":
288
+ steps = "imager,keys,messages,seed,switch,timers".split(",")
289
+
290
+ self.params["iteration"] += 1
291
+
292
+ hardware.pulse("loop", 0)
293
+
294
+ for enabled, step_ in zip(
295
+ [
296
+ "keys" in steps,
297
+ "messages" in steps,
298
+ "timers" in steps,
299
+ "seed" in steps,
300
+ "imager" in steps,
301
+ ],
302
+ [
303
+ self.check_keys,
304
+ self.check_messages,
305
+ self.check_timers,
306
+ self.check_seed,
307
+ self.check_imager,
308
+ ],
309
+ ):
310
+ if not enabled:
311
+ continue
312
+ output = step_()
313
+ if output in [False, True]:
314
+ return output
315
+
316
+ hardware.clock()
317
+
318
+ return True
@@ -0,0 +1,22 @@
1
+ import os
2
+ from typing import List
3
+
4
+ from bluer_ai.env import ABCLI_PATH_IGNORE
5
+ from bluer_objects import file
6
+
7
+ from bluer_sbc.logger import logger
8
+
9
+
10
+ def reply_to_bash(
11
+ status: str, # exit/reboot/seed/shutdown/update
12
+ content: List[str] = [],
13
+ ) -> bool:
14
+ logger.info(f"session.reply_to_bash({status}).")
15
+
16
+ return file.save_text(
17
+ filename=os.path.join(
18
+ ABCLI_PATH_IGNORE,
19
+ f"session_reply_{status}",
20
+ ),
21
+ text=content,
22
+ )
bluer_sbc/urls.py ADDED
@@ -0,0 +1 @@
1
+ urlpatterns = []
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: bluer_sbc
3
+ Version: 8.3.1
4
+ Summary: 🌀 AI for single board computers.
5
+ Home-page: https://github.com/kamangir/bluer-sbc
6
+ Author: Arash Abadpour (Kamangir)
7
+ Author-email: arash@kamangir.net
8
+ License: Public Domain
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Unix Shell
11
+ Classifier: License :: Public Domain
12
+ Classifier: Operating System :: OS Independent
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: bluer_ai
16
+ Requires-Dist: bluer_sbc
17
+ Dynamic: author
18
+ Dynamic: author-email
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: home-page
23
+ Dynamic: license
24
+ Dynamic: license-file
25
+ Dynamic: requires-dist
26
+ Dynamic: summary
27
+
28
+ # 🌀 bluer-sbc
29
+
30
+ 🌀 `bluer-sbc` is a [`bluer-ai`](https://github.com/kamangir/bluer-ai) plugin for edge computing on [single board computers](https://github.com/kamangir/blue-bracket).
31
+
32
+ ```bash
33
+ pip install bluer_sbc
34
+
35
+ # @env dot list
36
+ @env dot cp <env-name> local
37
+ ```
38
+
39
+ | | | | |
40
+ | --- | --- | --- | --- |
41
+ | [![image](https://github.com/kamangir/blue-bracket/raw/main/images/blue3-1.jpg)](https://github.com/kamangir/blue-bracket/blob/main/designs/blue3.md) | [![image](https://github.com/kamangir/blue-bracket/raw/main/images/chenar-grove-1.jpg)](https://github.com/kamangir/blue-bracket/blob/main/designs/chenar-grove.md) | [![image](https://github.com/kamangir/blue-bracket/raw/main/images/cube-1.jpg)](https://github.com/kamangir/blue-bracket/blob/main/designs/cube.md) | [![image](https://github.com/kamangir/blue-bracket/raw/main/images/eye_nano-1.jpg)](https://github.com/kamangir/blue-bracket/blob/main/designs/eye_nano.md) |
42
+
43
+
44
+
45
+ ---
46
+
47
+ > 🌀 [`blue-sbc`](https://github.com/kamangir/blue-sbc) for the [Global South](https://github.com/kamangir/bluer-south).
48
+
49
+ ---
50
+
51
+
52
+ [![pylint](https://github.com/kamangir/bluer-sbc/actions/workflows/pylint.yml/badge.svg)](https://github.com/kamangir/bluer-sbc/actions/workflows/pylint.yml) [![pytest](https://github.com/kamangir/bluer-sbc/actions/workflows/pytest.yml/badge.svg)](https://github.com/kamangir/bluer-sbc/actions/workflows/pytest.yml) [![bashtest](https://github.com/kamangir/bluer-sbc/actions/workflows/bashtest.yml/badge.svg)](https://github.com/kamangir/bluer-sbc/actions/workflows/bashtest.yml) [![PyPI version](https://img.shields.io/pypi/v/bluer-sbc.svg)](https://pypi.org/project/bluer-sbc/) [![PyPI - Downloads](https://img.shields.io/pypi/dd/bluer-sbc)](https://pypistats.org/packages/bluer-sbc)
53
+
54
+ built by 🌀 [`bluer_options-5.32.1`](https://github.com/kamangir/awesome-bash-cli), based on 🌀 [`bluer_sbc-8.3.1`](https://github.com/kamangir/bluer-sbc).
55
+
56
+
57
+
58
+ built by 🌀 [`blueness-3.96.1`](https://github.com/kamangir/blueness).