pyhubbledemo 0.0.2__tar.gz → 0.0.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyhubbledemo
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Hubble demo functionality for flashing and provisioning boards
5
5
  Author-email: Paul Buckley <paul@hubble.com>
6
6
  License-Expression: Apache-2.0
@@ -9,6 +9,7 @@ Classifier: Operating System :: OS Independent
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Requires-Python: >=3.9
11
11
  Requires-Dist: click
12
+ Requires-Dist: intelhex
12
13
  Requires-Dist: petname
13
14
  Requires-Dist: pyelftools
14
15
  Requires-Dist: pyhubblenetwork
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pyhubbledemo"
7
- version = "0.0.2"
7
+ version = "0.0.4"
8
8
  requires-python = ">=3.9"
9
9
  authors = [
10
10
  { name="Paul Buckley", email="paul@hubble.com" },
@@ -22,6 +22,7 @@ dependencies = [
22
22
  "petname",
23
23
  "pylink-square",
24
24
  "pyelftools",
25
+ "intelhex",
25
26
  "pyhubblenetwork",
26
27
  ]
27
28
 
@@ -0,0 +1,13 @@
1
+ # hubbledemo/__init__.py
2
+
3
+ from .elfmgr import flash_elf, patch_elf, probe_device, convert_elf_to_hex
4
+ from .cloud import fetch_elf, fetch_metadata
5
+
6
+ __all__ = [
7
+ "flash_elf",
8
+ "fetch_elf",
9
+ "patch_elf",
10
+ "probe_device",
11
+ "fetch_metadata",
12
+ "convert_elf_to_hex",
13
+ ]
@@ -1,16 +1,10 @@
1
1
  from __future__ import annotations
2
-
2
+ from typing import Optional
3
3
  import click
4
4
  import os
5
- import json
6
- import time
7
- import base64
8
- import sys
9
5
  import petname
10
- from datetime import timezone, datetime
11
- from typing import Optional
12
- from hubblenetwork import Organization, Credentials
13
- from hubbledemo import flash_elf, fetch_elf, patch_elf, probe_device
6
+ import hubbledemo
7
+ from hubblenetwork import Organization
14
8
 
15
9
 
16
10
  def _get_env_or_fail(name: str) -> str:
@@ -40,7 +34,7 @@ def cli() -> None:
40
34
 
41
35
  @cli.command("probe")
42
36
  def probe() -> None:
43
- if probe_device():
37
+ if hubbledemo.probe_device():
44
38
  click.echo("[SUCCESS] Device detected.")
45
39
  else:
46
40
  click.secho(
@@ -70,7 +64,16 @@ def probe() -> None:
70
64
  help="Token (if not using HUBBLE_API_TOKEN env var)",
71
65
  )
72
66
  def flash(board: str, name: str = None, org_id: str = None, token: str = None) -> None:
73
- if not probe_device():
67
+ click.secho("[INFO] Getting metadata... ", nl=False)
68
+ metadata = hubbledemo.fetch_metadata()
69
+ click.secho("[SUCCESS]")
70
+
71
+ if board not in metadata:
72
+ click.secho("[ERROR] Unsupported board selected!", fg="red",err=True)
73
+ return 2
74
+
75
+ # If using jlink, we want to make sure early on that a device is connected
76
+ if metadata[board]["method"] == "jlink-flash" and not hubbledemo.probe_device():
74
77
  click.secho(
75
78
  "[ERROR] Failed to connect to device. Check your connection.",
76
79
  fg="red",
@@ -79,15 +82,15 @@ def flash(board: str, name: str = None, org_id: str = None, token: str = None) -
79
82
  return 2
80
83
 
81
84
  org_id, token = _get_org_and_token(org_id, token)
82
- org = Organization(Credentials(org_id=org_id, api_token=token))
85
+ org = Organization(org_id=org_id, api_token=token)
83
86
 
84
- click.secho(f"[INFO] Organization info acquired:")
87
+ click.secho("[INFO] Organization info acquired:")
85
88
  click.secho(f"\tID: {org.credentials.org_id}")
86
89
  click.secho(f"\tName: {org.name}")
87
90
  click.secho(f"\tEnvironment: {org.env.name}")
88
91
 
89
- click.secho(f'[INFO] Registering new device"... ', nl=False)
90
- device = org.register_device()
92
+ click.secho('[INFO] Registering new device"... ', nl=False)
93
+ device = org.register_device(encryption=metadata[board]["encryption"] if "encryption" in metadata[board] else None)
91
94
  click.secho("[SUCCESS]")
92
95
  click.secho(f"\tDevice ID: {device.id}")
93
96
  click.secho(f"\tDevice Key: {device.key}")
@@ -95,24 +98,39 @@ def flash(board: str, name: str = None, org_id: str = None, token: str = None) -
95
98
  if not name:
96
99
  name = petname.generate(words=3)
97
100
  click.secho(f'[INFO] No name supplied. Naming device "{name}"')
98
- click.secho(f"[INFO] Setting device name... ", nl=False)
101
+ click.secho("[INFO] Setting device name... ", nl=False)
99
102
  org.set_device_name(device_id=device.id, name=name)
100
103
  click.secho("[SUCCESS]")
101
104
 
102
105
  click.secho(f"[INFO] Retrieving binary for {board}... ", nl=False)
103
- buf = fetch_elf(board=board)
106
+ buf = hubbledemo.fetch_elf(board=board)
104
107
  click.secho("[SUCCESS]")
105
108
 
106
109
  click.secho("[INFO] Patching key + UTC into binary... ", nl=False)
107
- patch_elf(buf, device)
108
- click.secho("[SUCCESS]")
109
-
110
- click.secho("[INFO] Flashing binary onto device... ", nl=False)
111
- flash_elf(board=board, buf=buf)
110
+ hubbledemo.patch_elf(buf, device)
112
111
  click.secho("[SUCCESS]")
113
112
 
114
- click.secho(f"\n{board} successfully flashed and provisioned!")
115
-
113
+ # Depending on method, deliver how we will do provisioning.
114
+ if metadata[board]["method"] == "jlink-flash":
115
+ click.secho("[INFO] Flashing binary onto device... ", nl=False)
116
+ hubbledemo.flash_elf(board=board, buf=buf, jlink_device=metadata[board]["jlink_device"])
117
+ click.secho("[SUCCESS]")
118
+
119
+ click.secho(f"\n{board} successfully flashed and provisioned!")
120
+ elif metadata[board]["method"] == "generate-hex":
121
+ click.secho("[INFO] Generating hex file... ", nl=False)
122
+ hubbledemo.convert_elf_to_hex(buf=buf, filename=name)
123
+ click.secho("[SUCCESS]")
124
+ click.secho(
125
+ f"\nHex file written to \"{os.getcwd()}/{name}.hex\"",
126
+ bg='blue', fg='white')
127
+ else:
128
+ click.secho(
129
+ "[ERROR] Unknown method for provisioning board.",
130
+ fg="red",
131
+ err=True,
132
+ )
133
+ return 2
116
134
 
117
135
  def main(argv: Optional[list[str]] = None) -> int:
118
136
  """
@@ -130,7 +148,6 @@ def main(argv: Optional[list[str]] = None) -> int:
130
148
  return 2
131
149
  return 0
132
150
 
133
-
134
151
  if __name__ == "__main__":
135
152
  # Forward command-line args (excluding the program name) to main()
136
153
  raise SystemExit(main())
@@ -0,0 +1,154 @@
1
+ from typing import Optional, Any
2
+ import io
3
+ import os
4
+ import requests
5
+ import json
6
+ import time
7
+ from pathlib import Path
8
+
9
+ _ARTIFACT_BASE_URL = (
10
+ "https://raw.githubusercontent.com/HubbleNetwork/hubble-tldm/master/merge"
11
+ )
12
+
13
+ _METADATA_FILENAME = "md.json"
14
+
15
+ def fetch_elf(board: str, timeout: float = 20.0) -> io.BytesIO:
16
+ """
17
+ Download the board-specific ELF from HubbleNetwork/hubble-tldm/merge and
18
+ return it as an io.BytesIO.
19
+
20
+ Parameters
21
+ ----------
22
+ board_name : str
23
+ Board identifier (e.g. 'nrf21540dk', 'xg24_ek2703a', 'xg22_ek4108a').
24
+ timeout : float
25
+ Requests timeout in seconds (connect + read).
26
+
27
+ Returns
28
+ -------
29
+ io.BytesIO
30
+ Raw bytes of the .elf file
31
+
32
+ Raises
33
+ ------
34
+ ValueError
35
+ If the board is not supported or name is malformed.
36
+ FileNotFoundError
37
+ If the expected ELF file does not exist in the merge directory.
38
+ ConnectionError
39
+ On network, HTTP, or parsing failures.
40
+ """
41
+ if not isinstance(board, str) or not board.strip():
42
+ raise ValueError("board must be a non-empty string")
43
+
44
+ # If we have a local override, just use that
45
+ local_file = os.getenv("HUBBLE_DEMO_ELF_FILE")
46
+ if local_file:
47
+ return io.BytesIO(Path(local_file).read_bytes())
48
+
49
+ # Give option (for development) to pull binary from elsewhere
50
+ val = os.getenv("HUBBLE_DEMO_URL_OVERRIDE")
51
+ if val:
52
+ base_url = val
53
+ else:
54
+ base_url = _ARTIFACT_BASE_URL
55
+
56
+ url = f"{base_url}/{board}.elf"
57
+
58
+ _RETRY_STATUS = {429, 500, 502, 503, 504}
59
+ retries = 5
60
+
61
+ last_err: Optional[Exception] = None
62
+ for attempt in range(1, max(1, retries) + 1):
63
+ try:
64
+ resp = requests.get(url, timeout=5)
65
+
66
+ if resp.status_code == 404:
67
+ # Not found is definitive; don't bother retrying
68
+ raise FileNotFoundError(f"No ELF for board '{board}' at {url}")
69
+
70
+ # Retry transient status codes (unless it's the final attempt)
71
+ if resp.status_code in _RETRY_STATUS and attempt < retries:
72
+ sleep_s = (2 ** (attempt - 1))
73
+ time.sleep(sleep_s)
74
+ continue
75
+
76
+ # Raise for other non-OK codes
77
+ resp.raise_for_status()
78
+
79
+ # Basic sanity checks: content-type and size
80
+ ctype = (resp.headers.get("Content-Type") or "").lower()
81
+ if "html" in ctype:
82
+ raise ValueError(f"Expected ELF bytes, got {ctype} from {url}")
83
+
84
+ return io.BytesIO(resp.content)
85
+
86
+ except (requests.Timeout, requests.ConnectionError) as e:
87
+ last_err = e
88
+ if attempt < retries:
89
+ sleep_s = (2 ** (attempt - 1))
90
+ time.sleep(sleep_s)
91
+ continue
92
+ raise ConnectionError(f"Failed to download ELF from {url}: {e}") from e
93
+
94
+ except Exception:
95
+ raise
96
+
97
+ # Should not reach here; defensive:
98
+ raise ConnectionError(f"Failed to download ELF from {url}: {last_err}")
99
+
100
+ def fetch_metadata() -> Any:
101
+ """
102
+ Downloads the metadata about devices and returns it
103
+ """
104
+ # If we have a local override, just use that
105
+ local_file = os.getenv("HUBBLE_DEMO_METADATA_FILE")
106
+ if local_file:
107
+ with open(local_file, "r", encoding="utf-8") as f:
108
+ return json.load(f)
109
+
110
+ # Give option (for development) to pull binary from elsewhere
111
+ val = os.getenv("HUBBLE_DEMO_URL_OVERRIDE")
112
+ if val:
113
+ base_url = val
114
+ else:
115
+ base_url = _ARTIFACT_BASE_URL
116
+
117
+ url = f"{base_url}/{_METADATA_FILENAME}"
118
+
119
+ _RETRY_STATUS = {429, 500, 502, 503, 504}
120
+ retries = 5
121
+
122
+ last_err: Optional[Exception] = None
123
+ for attempt in range(1, max(1, retries) + 1):
124
+ try:
125
+ resp = requests.get(url, timeout=5)
126
+
127
+ if resp.status_code == 404:
128
+ # Not found is definitive; don't bother retrying
129
+ raise FileNotFoundError("No metadata file found for devices")
130
+
131
+ # Retry transient status codes (unless it's the final attempt)
132
+ if resp.status_code in _RETRY_STATUS and attempt < retries:
133
+ sleep_s = 2 ** (attempt - 1)
134
+ time.sleep(sleep_s)
135
+ continue
136
+
137
+ # Raise for other non-OK codes
138
+ resp.raise_for_status()
139
+
140
+ return json.loads(resp.text)
141
+
142
+ except (requests.Timeout, requests.ConnectionError) as e:
143
+ last_err = e
144
+ if attempt < retries:
145
+ sleep_s = (2 ** (attempt - 1))
146
+ time.sleep(sleep_s)
147
+ continue
148
+ raise ConnectionError(f"Failed to download metadata file from {url}: {e}") from e
149
+
150
+ except Exception:
151
+ raise
152
+
153
+ # Should not reach here; defensive:
154
+ raise ConnectionError(f"Failed to download metadata from {url}: {last_err}")
@@ -5,28 +5,12 @@ from hubblenetwork import Device
5
5
  import io
6
6
  import os
7
7
  import base64
8
- import requests
9
8
  import time
10
9
  import tempfile
11
10
  import pylink
12
- from pathlib import Path
13
- from typing import Dict
14
11
  from elftools.elf.elffile import ELFFile
15
12
  from elftools.elf.sections import SymbolTableSection
16
-
17
-
18
- _ELF_BASE_URL = (
19
- "https://raw.githubusercontent.com/HubbleNetwork/hubble-tldm/master/merge"
20
- )
21
-
22
- # Map boards to J-Link "device" strings (extend as needed)
23
- _BOARD_TO_JLINK_DEVICE: Dict[str, str] = {
24
- "nrf52dk": "nRF52832_xxAA",
25
- "nrf52840dk": "nRF52840_xxAA",
26
- "nrf21540dk": "nRF52840_xxAA",
27
- "xg24_ek2703a": "EFR32MG24BxxxF1536",
28
- "xg22_ek4108a": "EFR32MG22CxxxF512",
29
- }
13
+ from intelhex import IntelHex
30
14
 
31
15
 
32
16
  def _compute_file_offset(sym, sec) -> int:
@@ -116,7 +100,7 @@ def probe_device() -> bool:
116
100
  return jlink.num_connected_emulators() > 0
117
101
 
118
102
 
119
- def flash_elf(buf: io.BytesIO, board: str) -> None:
103
+ def flash_elf(buf: io.BytesIO, board: str, jlink_device) -> None:
120
104
  """
121
105
  Flash an ELF image (held in a BytesIO) to an nRF52832_xxAA using pylink.
122
106
  Creates a temporary .elf on disk (needed by jlink.flash_file) and deletes it afterwards.
@@ -130,7 +114,6 @@ def flash_elf(buf: io.BytesIO, board: str) -> None:
130
114
  RuntimeError: on J-Link connection or flashing failure.
131
115
  """
132
116
  speed_khz = 4000
133
- device = _BOARD_TO_JLINK_DEVICE.get(board.strip().lower())
134
117
 
135
118
  # Write the buffer to a real temp file so flash_file can read it (works on all OSes)
136
119
  tmp_path = None
@@ -154,7 +137,7 @@ def flash_elf(buf: io.BytesIO, board: str) -> None:
154
137
  try:
155
138
  jlink.open()
156
139
  jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
157
- jlink.connect(device, speed=speed_khz)
140
+ jlink.connect(jlink_device, speed=speed_khz)
158
141
 
159
142
  jlink.halt()
160
143
  jlink.flash_file(tmp_path, addr=None) # ELF contains its own load addresses
@@ -174,88 +157,23 @@ def flash_elf(buf: io.BytesIO, board: str) -> None:
174
157
  except OSError:
175
158
  pass
176
159
 
160
+ def convert_elf_to_hex(buf: io.BytesIO, filename: str):
161
+ ih = IntelHex()
177
162
 
178
- def fetch_elf(board: str, timeout: float = 20.0) -> io.BytesIO:
179
- """
180
- Download the board-specific ELF from HubbleNetwork/hubble-tldm/merge and
181
- return it as an io.BytesIO.
182
-
183
- Parameters
184
- ----------
185
- board_name : str
186
- Board identifier (e.g. 'nrf21540dk', 'xg24_ek2703a', 'xg22_ek4108a').
187
- timeout : float
188
- Requests timeout in seconds (connect + read).
189
-
190
- Returns
191
- -------
192
- io.BytesIO
193
- Raw bytes of the .elf file
194
-
195
- Raises
196
- ------
197
- ValueError
198
- If the board is not supported or name is malformed.
199
- FileNotFoundError
200
- If the expected ELF file does not exist in the merge directory.
201
- ConnectionError
202
- On network, HTTP, or parsing failures.
203
- """
204
- if not isinstance(board, str) or not board.strip():
205
- raise ValueError("board must be a non-empty string")
206
-
207
- # If we have a local override, just use that
208
- local_file = os.getenv("HUBBLE_DEMO_ELF_FILE")
209
- if local_file:
210
- return io.BytesIO(Path(local_file).read_bytes())
211
-
212
- # Give option (for development) to pull binary from elsewhere
213
- val = os.getenv("HUBBLE_DEMO_ELF_URL_OVERRIDE")
214
- if val:
215
- base_url = val
216
- else:
217
- base_url = _ELF_BASE_URL
218
-
219
- url = f"{base_url}/{board}.elf"
220
-
221
- _RETRY_STATUS = {429, 500, 502, 503, 504}
222
- retries = 5
223
-
224
- last_err: Optional[Exception] = None
225
- for attempt in range(1, max(1, retries) + 1):
226
- try:
227
- resp = requests.get(url, timeout=timeout)
228
-
229
- if resp.status_code == 404:
230
- # Not found is definitive; don't bother retrying
231
- raise FileNotFoundError(f"No ELF for board '{board}' at {url}")
232
-
233
- # Retry transient status codes (unless it's the final attempt)
234
- if resp.status_code in _RETRY_STATUS and attempt < retries:
235
- sleep_s = backoff * (2 ** (attempt - 1))
236
- time.sleep(sleep_s)
237
- continue
238
-
239
- # Raise for other non-OK codes
240
- resp.raise_for_status()
241
-
242
- # Basic sanity checks: content-type and size
243
- ctype = (resp.headers.get("Content-Type") or "").lower()
244
- if "html" in ctype:
245
- raise ValueError(f"Expected ELF bytes, got {ctype} from {url}")
163
+ buf.seek(0)
164
+ elf = ELFFile(buf)
246
165
 
247
- return io.BytesIO(resp.content)
166
+ for segment in elf.iter_segments():
167
+ if segment["p_type"] != "PT_LOAD":
168
+ continue
248
169
 
249
- except (requests.Timeout, requests.ConnectionError) as e:
250
- last_err = e
251
- if attempt < retries:
252
- sleep_s = backoff * (2 ** (attempt - 1))
253
- time.sleep(sleep_s)
254
- continue
255
- raise ConnectionError(f"Failed to download ELF from {url}: {e}") from e
170
+ data = segment.data()
171
+ # Prefer physical address if present, otherwise virtual
172
+ addr = segment['p_paddr'] if segment['p_paddr'] != 0 else segment['p_vaddr']
256
173
 
257
- except Exception as e:
258
- raise
174
+ # Store bytes into IntelHex at the proper address
175
+ for offset, b in enumerate(data):
176
+ ih[addr + offset] = b
177
+ #ih.frombytes(data, offset=addr)
259
178
 
260
- # Should not reach here; defensive:
261
- raise ConnectionError(f"Failed to download ELF from {url}: {last_err}")
179
+ ih.write_hex_file(str(filename + ".hex"))
@@ -1,10 +0,0 @@
1
- # hubbledemo/__init__.py
2
-
3
- from .elfmgr import flash_elf, fetch_elf, patch_elf, probe_device
4
-
5
- __all__ = [
6
- "flash_elf",
7
- "fetch_elf",
8
- "patch_elf",
9
- "probe_device",
10
- ]
File without changes
File without changes
File without changes