esp-batch-uploader 1.0.0__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.
- esp_batch_uploader/__init__.py +0 -0
- esp_batch_uploader/__main__.py +32 -0
- esp_batch_uploader/config.py +52 -0
- esp_batch_uploader/logger.py +30 -0
- esp_batch_uploader/uploader/__init__.py +0 -0
- esp_batch_uploader/uploader/batch_manager.py +42 -0
- esp_batch_uploader/uploader/discovery.py +52 -0
- esp_batch_uploader/uploader/upload_client.py +51 -0
- esp_batch_uploader/uploader/upload_runner.py +10 -0
- esp_batch_uploader-1.0.0.dist-info/METADATA +14 -0
- esp_batch_uploader-1.0.0.dist-info/RECORD +14 -0
- esp_batch_uploader-1.0.0.dist-info/WHEEL +5 -0
- esp_batch_uploader-1.0.0.dist-info/entry_points.txt +2 -0
- esp_batch_uploader-1.0.0.dist-info/top_level.txt +1 -0
File without changes
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# esp_batch_uploader/__main__.py
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from esp_batch_uploader.uploader.batch_manager import run_batches
|
5
|
+
from esp_batch_uploader.config import parse_args
|
6
|
+
from esp_batch_uploader.logger import setup_logger
|
7
|
+
|
8
|
+
async def main():
|
9
|
+
args, files_dir = parse_args()
|
10
|
+
logger, status_logger = setup_logger(args.verbose)
|
11
|
+
|
12
|
+
logger.info("Starting ESP32 batch uploader")
|
13
|
+
status_logger.info("Starting ESP32 batch uploader")
|
14
|
+
|
15
|
+
try:
|
16
|
+
await run_batches(
|
17
|
+
files_dir=files_dir,
|
18
|
+
batch_size=args.batch_size,
|
19
|
+
udp_port=args.udp_port,
|
20
|
+
logger=logger,
|
21
|
+
status_logger=status_logger
|
22
|
+
)
|
23
|
+
except Exception as e:
|
24
|
+
logger.exception("Unexpected error during batch upload")
|
25
|
+
status_logger.error(f"Unexpected error: {e}")
|
26
|
+
|
27
|
+
# 👇 This is the entry point for CLI installation
|
28
|
+
def main_entry():
|
29
|
+
asyncio.run(main())
|
30
|
+
|
31
|
+
if __name__ == "__main__":
|
32
|
+
main_entry()
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
|
4
|
+
def prompt_yes_no(prompt):
|
5
|
+
while True:
|
6
|
+
response = input(f"{prompt} (y/N): ").strip().lower()
|
7
|
+
if response == "":
|
8
|
+
return False # default to 'n'
|
9
|
+
if response in ("y", "n"):
|
10
|
+
return response == "y"
|
11
|
+
print("Please enter 'y' or 'n'.")
|
12
|
+
|
13
|
+
def prompt_int(prompt, default, min_val, max_val):
|
14
|
+
while True:
|
15
|
+
response = input(f"{prompt} [{default}]: ").strip()
|
16
|
+
if not response:
|
17
|
+
return default
|
18
|
+
if response.isdigit():
|
19
|
+
val = int(response)
|
20
|
+
if min_val <= val <= max_val:
|
21
|
+
return val
|
22
|
+
print(f"Please enter a number between {min_val} and {max_val}.")
|
23
|
+
|
24
|
+
def prompt_port(prompt, default):
|
25
|
+
while True:
|
26
|
+
response = input(f"{prompt} [{default}]: ").strip()
|
27
|
+
if not response:
|
28
|
+
return default
|
29
|
+
if response.isdigit() and 1 <= int(response) <= 65535:
|
30
|
+
return int(response)
|
31
|
+
print("Please enter a valid port number (1–65535).")
|
32
|
+
|
33
|
+
def parse_args():
|
34
|
+
files_dir = os.getcwd() # Current working directory
|
35
|
+
print(f"[INFO] Serving files from: {files_dir}")
|
36
|
+
|
37
|
+
verbose = prompt_yes_no("Enable verbose logging?")
|
38
|
+
batch_size = prompt_int("Enter batch size", default=5, min_val=1, max_val=20)
|
39
|
+
http_port = prompt_port("HTTP port", default=8000)
|
40
|
+
udp_port = prompt_port("UDP port", default=5757)
|
41
|
+
|
42
|
+
class Args:
|
43
|
+
pass
|
44
|
+
|
45
|
+
args = Args()
|
46
|
+
args.verbose = verbose
|
47
|
+
args.batch_size = batch_size
|
48
|
+
args.http_port = http_port
|
49
|
+
args.udp_port = udp_port
|
50
|
+
args.directory = files_dir
|
51
|
+
|
52
|
+
return args, files_dir
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import logging
|
2
|
+
import datetime
|
3
|
+
import os
|
4
|
+
|
5
|
+
def setup_logger(verbose=False):
|
6
|
+
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
7
|
+
logs_dir = os.path.join(os.getcwd(), 'logs')
|
8
|
+
os.makedirs(logs_dir, exist_ok=True)
|
9
|
+
|
10
|
+
traceback_log = os.path.join(logs_dir, f"traceback-file-transfer-{timestamp}.txt")
|
11
|
+
status_log = os.path.join(logs_dir, f"status-file-transfer-{timestamp}.txt")
|
12
|
+
|
13
|
+
logging.basicConfig(
|
14
|
+
level=logging.DEBUG if verbose else logging.INFO,
|
15
|
+
format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
|
16
|
+
handlers=[
|
17
|
+
logging.FileHandler(traceback_log, encoding='utf-8'),
|
18
|
+
logging.StreamHandler()
|
19
|
+
]
|
20
|
+
)
|
21
|
+
|
22
|
+
status_logger = logging.getLogger("StatusLogger")
|
23
|
+
status_handler = logging.FileHandler(status_log, encoding='utf-8')
|
24
|
+
status_handler.setLevel(logging.INFO)
|
25
|
+
status_handler.setFormatter(logging.Formatter("%(asctime)s [STATUS] %(message)s"))
|
26
|
+
status_logger.addHandler(status_handler)
|
27
|
+
status_logger.propagate = False
|
28
|
+
status_logger.setLevel(logging.INFO)
|
29
|
+
|
30
|
+
return logging.getLogger("ESP32BatchServer"), status_logger
|
File without changes
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# esp_batch_uploader/uploader/batch_manager.py
|
2
|
+
|
3
|
+
import os
|
4
|
+
from esp_batch_uploader.uploader.discovery import discover_esp32_devices
|
5
|
+
from esp_batch_uploader.uploader.upload_runner import upload_to_batch
|
6
|
+
|
7
|
+
|
8
|
+
def split_batches(devices, batch_size):
|
9
|
+
return [devices[i:i + batch_size] for i in range(0, len(devices), batch_size)]
|
10
|
+
|
11
|
+
|
12
|
+
async def run_batches(files_dir, batch_size, udp_port, logger, status_logger):
|
13
|
+
logger.info("Discovering ESP32 devices...")
|
14
|
+
devices = await discover_esp32_devices(udp_port)
|
15
|
+
if not devices:
|
16
|
+
logger.error("No devices found.")
|
17
|
+
status_logger.info("No devices found during discovery")
|
18
|
+
return
|
19
|
+
|
20
|
+
logger.info(f"Discovered {len(devices)} device(s): {devices}")
|
21
|
+
status_logger.info(f"Devices discovered: {devices}")
|
22
|
+
|
23
|
+
file_paths = [
|
24
|
+
os.path.join(files_dir, f)
|
25
|
+
for f in os.listdir(files_dir)
|
26
|
+
if os.path.isfile(os.path.join(files_dir, f))
|
27
|
+
]
|
28
|
+
|
29
|
+
if not file_paths:
|
30
|
+
logger.error("No files to upload.")
|
31
|
+
return
|
32
|
+
|
33
|
+
batches = split_batches(devices, batch_size)
|
34
|
+
logger.info(f"Split into {len(batches)} batch(es) of size {batch_size}")
|
35
|
+
|
36
|
+
for i, batch in enumerate(batches, 1):
|
37
|
+
logger.info(f"Processing batch {i}/{len(batches)}: {batch}")
|
38
|
+
status_logger.info(f"Uploading to batch {i}: {batch}")
|
39
|
+
await upload_to_batch(batch, file_paths, logger, status_logger)
|
40
|
+
|
41
|
+
logger.info("All batches processed.")
|
42
|
+
status_logger.info("✅ All uploads completed")
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import asyncio
|
2
|
+
import socket
|
3
|
+
import logging
|
4
|
+
|
5
|
+
logger = logging.getLogger("ESP32BatchServer.Discovery")
|
6
|
+
|
7
|
+
class UDPProtocol(asyncio.DatagramProtocol):
|
8
|
+
def __init__(self, queue):
|
9
|
+
self.queue = queue
|
10
|
+
|
11
|
+
def datagram_received(self, data, addr):
|
12
|
+
ip, _ = addr
|
13
|
+
msg = data.decode().strip()
|
14
|
+
logger.debug(f"Received UDP from {ip}: {msg}")
|
15
|
+
self.queue.put_nowait((ip, msg))
|
16
|
+
|
17
|
+
|
18
|
+
async def discover_esp32_devices(udp_port, timeout=5.0, http_port=8000):
|
19
|
+
loop = asyncio.get_running_loop()
|
20
|
+
queue = asyncio.Queue()
|
21
|
+
|
22
|
+
# Start UDP listener
|
23
|
+
transport, _ = await loop.create_datagram_endpoint(
|
24
|
+
lambda: UDPProtocol(queue),
|
25
|
+
local_addr=("0.0.0.0", udp_port)
|
26
|
+
)
|
27
|
+
|
28
|
+
# === Broadcast discovery message ===
|
29
|
+
broadcast_msg = f"SERVER:{socket.gethostbyname(socket.gethostname())}:{http_port}"
|
30
|
+
logger.info(f"Broadcasting: {broadcast_msg}")
|
31
|
+
|
32
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
33
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
34
|
+
sock.sendto(broadcast_msg.encode(), ("<broadcast>", udp_port))
|
35
|
+
|
36
|
+
logger.info(f"Listening for ESP32 responses on UDP port {udp_port} for {timeout}s")
|
37
|
+
|
38
|
+
discovered = set()
|
39
|
+
try:
|
40
|
+
start = loop.time()
|
41
|
+
while loop.time() - start < timeout:
|
42
|
+
try:
|
43
|
+
ip, msg = await asyncio.wait_for(queue.get(), timeout=timeout - (loop.time() - start))
|
44
|
+
if msg.startswith("ESP32_RESPONSE"):
|
45
|
+
discovered.add(ip)
|
46
|
+
logger.info(f"Discovered ESP32 at {ip}")
|
47
|
+
except asyncio.TimeoutError:
|
48
|
+
break
|
49
|
+
finally:
|
50
|
+
transport.close()
|
51
|
+
|
52
|
+
return list(discovered)
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import os
|
2
|
+
import time
|
3
|
+
import aiohttp
|
4
|
+
import asyncio
|
5
|
+
|
6
|
+
class UploadClient:
|
7
|
+
def __init__(self, ip, logger, status_logger):
|
8
|
+
self.ip = ip
|
9
|
+
self.logger = logger
|
10
|
+
self.status_logger = status_logger
|
11
|
+
|
12
|
+
async def upload_file(self, session, file_path, remote_name):
|
13
|
+
if not os.path.exists(file_path):
|
14
|
+
self.logger.error(f"File does not exist: {file_path}")
|
15
|
+
return False
|
16
|
+
|
17
|
+
url = f"http://{self.ip}/upload/{remote_name}"
|
18
|
+
file_size = os.path.getsize(file_path)
|
19
|
+
|
20
|
+
self.logger.info(f"Uploading to {url} ({file_size / 1024:.2f} KB)")
|
21
|
+
self.status_logger.info(f"Start upload {remote_name} to {self.ip}")
|
22
|
+
|
23
|
+
try:
|
24
|
+
start_time = time.time()
|
25
|
+
with open(file_path, 'rb') as f:
|
26
|
+
async with session.post(url, data=f) as resp:
|
27
|
+
duration = time.time() - start_time
|
28
|
+
if resp.status == 200:
|
29
|
+
speed = file_size / duration if duration > 0 else 0
|
30
|
+
self.logger.info(f"✅ Uploaded {remote_name} to {self.ip} in {duration:.2f}s ({speed/1024/1024:.2f} MB/s)")
|
31
|
+
self.status_logger.info(f"✅ {self.ip} <- {remote_name} OK")
|
32
|
+
return True
|
33
|
+
else:
|
34
|
+
text = await resp.text()
|
35
|
+
self.logger.error(f"❌ Upload failed: {resp.status} - {text}")
|
36
|
+
self.status_logger.info(f"❌ {self.ip} <- {remote_name} FAILED")
|
37
|
+
return False
|
38
|
+
except Exception as e:
|
39
|
+
self.logger.exception(f"Upload error to {self.ip}: {e}")
|
40
|
+
self.status_logger.info(f"❌ {self.ip} <- {remote_name} ERROR")
|
41
|
+
return False
|
42
|
+
|
43
|
+
async def upload_files(self, file_paths):
|
44
|
+
async with aiohttp.ClientSession() as session:
|
45
|
+
for path in file_paths:
|
46
|
+
remote_name = os.path.basename(path)
|
47
|
+
success = await self.upload_file(session, path, remote_name)
|
48
|
+
if not success:
|
49
|
+
self.logger.warning(f"Retrying {remote_name} to {self.ip} after failure")
|
50
|
+
await asyncio.sleep(2)
|
51
|
+
await self.upload_file(session, path, remote_name)
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import asyncio
|
2
|
+
from esp_batch_uploader.uploader.upload_client import UploadClient
|
3
|
+
|
4
|
+
async def upload_to_batch(batch_ips, file_paths, logger, status_logger):
|
5
|
+
tasks = []
|
6
|
+
for ip in batch_ips:
|
7
|
+
client = UploadClient(ip, logger, status_logger)
|
8
|
+
task = asyncio.create_task(client.upload_files(file_paths))
|
9
|
+
tasks.append(task)
|
10
|
+
await asyncio.gather(*tasks)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: esp-batch-uploader
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: Batch file uploader to ESP32 devices over HTTP with UDP discovery
|
5
|
+
Author: Abdullah Bajwa
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
7
|
+
Classifier: Operating System :: OS Independent
|
8
|
+
Requires-Python: >=3.7
|
9
|
+
Requires-Dist: aiohttp
|
10
|
+
Dynamic: author
|
11
|
+
Dynamic: classifier
|
12
|
+
Dynamic: requires-dist
|
13
|
+
Dynamic: requires-python
|
14
|
+
Dynamic: summary
|
@@ -0,0 +1,14 @@
|
|
1
|
+
esp_batch_uploader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
esp_batch_uploader/__main__.py,sha256=zRoC4qGBY-SIrFcelLk51p_ZnmRTNGViMc3HIY3a-iA,939
|
3
|
+
esp_batch_uploader/config.py,sha256=mAGATYcVsHMpKSLaEV-nkya_e_9rpUhowQA3chgUWsM,1619
|
4
|
+
esp_batch_uploader/logger.py,sha256=ZOaQYVN6uAKGNSlloWqO2OCTXOVCrfW3VIV-HLm7LxY,1132
|
5
|
+
esp_batch_uploader/uploader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
esp_batch_uploader/uploader/batch_manager.py,sha256=IF8RrEOC0vEceTDruAN2auv0DMFpd93x8jjAeEldvrs,1523
|
7
|
+
esp_batch_uploader/uploader/discovery.py,sha256=L2Af2PylFAuydZbRnRFAMKO9fYDqljZS4KHm5BD3tfY,1696
|
8
|
+
esp_batch_uploader/uploader/upload_client.py,sha256=-9DPrZS9eVT_cmQ80TWKCvCTxaGUo_7SqM8RA3-O698,2262
|
9
|
+
esp_batch_uploader/uploader/upload_runner.py,sha256=_zpJeVqrIoLxhppITFfZyNsoVRmmH-ny3_IzZqAPXuA,381
|
10
|
+
esp_batch_uploader-1.0.0.dist-info/METADATA,sha256=I-itsu1y_me93zNHsJwEmBejv_ogx8fs7keAPWikLr8,416
|
11
|
+
esp_batch_uploader-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
12
|
+
esp_batch_uploader-1.0.0.dist-info/entry_points.txt,sha256=vfXlI_I2WrLcCX2az__iMQSSLZYhdYxhAIJwqj_1AfQ,70
|
13
|
+
esp_batch_uploader-1.0.0.dist-info/top_level.txt,sha256=atphBgAqEAQs-qumkBOCkhfGBeq3E1paULvOqcjnbDM,19
|
14
|
+
esp_batch_uploader-1.0.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
esp_batch_uploader
|