Skip to content

Commit 1e0b450

Browse files
glassesRecord config update and adding compatiblity for concurrent recording sessions
1 parent 79bfc81 commit 1e0b450

File tree

4 files changed

+152
-67
lines changed

4 files changed

+152
-67
lines changed

src/glassesRecord/README.md

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,19 @@ To summarize the important steps:
3535

3636
## Usage
3737

38-
1. Check the network config parameters in `config.json` so the devices could be found on network.
38+
1. Install the requirements with `pip install -r requirements.txt`
3939

40-
By default, SocialEyes will look for a range of IP addresses provided using the following config parameters:
40+
2. Check the network config parameters in `config.json` so the devices could be found on network.
41+
42+
3. Configure other parameters in `config.json`. See the next section for parameter descriptions.
43+
44+
4. Run the module with `python3 main.py`
45+
46+
## Config
47+
48+
The following parameters can be set in config.json to configure your recording session:
49+
50+
- By default, SocialEyes will look for a range of IP addresses provided using the following config parameters:
4151

4252
```
4353
{
@@ -62,12 +72,38 @@ To summarize the important steps:
6272
"192.168.50.110"
6373
]
6474
}
65-
```
75+
```
76+
77+
- The `"logs"` section lets you set:
78+
- `"path"`: Directory where log files will be stored.
79+
- `"level"`: Logging verbosity (e.g., `"INFO"`, `"DEBUG"`).
80+
- `"interval"`: Time interval (in seconds) for periodic logging or updates.
6681

82+
- The `"single_session_mode"` option controls whether the recording session will involve a single recording start (and stop) for all releavant devices (`true`) OR it requires multiple concurrent recording starts with different batches of devices (`false`).
6783

68-
2. Install the requirements with `pip install -r requirements.txt`
84+
- The `"update_times"` section allows you to specify custom update intervals for different batches of device metrics.
6985

70-
3. Run the module with `python3 main.py`
86+
```
87+
**Batch 1 Metrics**
88+
89+
PING (Device network latency)
90+
WIFI (WiFi networks)
91+
ADB (ADB connection status)
92+
USB (USB connections)
93+
RED_INDICATOR (Red light indicator events)
94+
Storage (Free disk space)
95+
Battery (Battery level)
96+
App (Active app status)
97+
98+
**Batch 2 Metrics**
99+
100+
API (App API status)
101+
RTSP (RTSP streaming status)
102+
PL_Rec (Recording state)
103+
PL_Rec_ID (Recording ID)
104+
PL_Rec_Duration (Recording duration)
105+
Device (App device name/identifier)
106+
```
71107

72108
### Debugging and Logs
73109
- Run the following command to start the application in development mode: `textual run --dev main.py`

src/glassesRecord/config.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
"192.168.2.124",
1212
"192.168.2.016"
1313
],
14-
"paths": {
15-
"logs": "logs/"
16-
}
14+
"logs": {
15+
"path": "logs/",
16+
"level": "INFO",
17+
"interval": 10
18+
},
19+
"update_times": {
20+
"batch1": 3,
21+
"batch2": 5
22+
},
23+
"single_session_mode": true
1724
}

src/glassesRecord/device.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,6 @@
2222
import logging
2323
import pytz
2424
import html
25-
26-
27-
base_dir = os.path.abspath(os.path.dirname(__file__))
28-
os.makedirs(os.path.join(base_dir,"logs"), exist_ok=True)
29-
30-
logging.basicConfig(
31-
filename=os.path.join(base_dir,"logs", str(datetime.now().strftime('%y%m%dT%H%M%S')) + '_log.txt'),
32-
encoding='utf-8',
33-
level=logging.INFO, # change to DEBUG if required
34-
format='[%(asctime)s] %(levelname)s [%(name)s] %(message)s')
3525
from enum import Enum
3626

3727
class RecordingState(Enum):

src/glassesRecord/main.py

Lines changed: 101 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,8 @@
2121
from device import Device
2222
from OffsetLogger import OffsetLogger
2323
from config import config
24-
2524
import logging
26-
logging.getLogger('pupil_labs.realtime_api.time_echo').setLevel(logging.ERROR)
27-
logger = logging.getLogger('main_device_monitor')
25+
2826

2927
#Define column fields
3028
COLUMNS = ("Check", "Device", "IP", "PING", "WIFI", "ADB", "Battery", "Storage", "USB", "RED_INDICATOR",
@@ -40,9 +38,12 @@ class TableApp(App):
4038
row_keys = []
4139
column_keys = []
4240
devices = []
43-
offset_logger: OffsetLogger = None
44-
file_ts = datetime.now().strftime('%y%m%dT%H%M%S')
45-
events_file = os.path.join(config["paths"]["logs"], f"{file_ts}_events.json")
41+
offset_logger = None
42+
logger = None
43+
session_id = datetime.now().strftime('%y%m%dT%H%M%S') #Session ID created using timestamp; could also be created using UUID, user input, etc.
44+
session_dir = os.path.join(config["logs"]["path"], session_id)
45+
events_file = os.path.join(session_dir, "events.json")
46+
single_session_mode = config["single_session_mode"]
4647

4748
restart_app_in_progress = False
4849

@@ -53,15 +54,29 @@ def on_mount(self) -> None:
5354
Sets up the device table, generates IP addresses for devices, and schedules
5455
periodic updates for various metrics related to the devices.
5556
"""
56-
os.makedirs(config["paths"]["logs"], exist_ok=True)
57+
58+
## Setup session directory and logging
59+
try:
60+
os.makedirs(self.session_dir)
61+
except FileExistsError:
62+
print("Session folder already exists, please try again.")
63+
self.app.exit()
64+
logging.basicConfig(
65+
filename=os.path.join(self.session_dir,'logs.txt'),
66+
encoding='utf-8',
67+
level=config["logs"]["level"], # change to DEBUG if required
68+
format='[%(asctime)s] %(levelname)s [%(name)s] %(message)s')
69+
logging.getLogger('pupil_labs.realtime_api.time_echo').setLevel(logging.ERROR)
70+
self.logger = logging.getLogger('glassesRecord_TUI')
71+
72+
# Setup app theme and table
5773
self.theme = "textual-dark"
5874
table = self.query_one(SelectableRowsDataTable)
5975
table.cursor_type = "row"
6076
self.column_keys = table.add_columns(*COLUMNS) #is_valid_column_index(self, column_index) can be used to verify
6177

62-
self.devices = []
63-
6478
#Generate ip addrs of devices using config parameters (looks for a host_id range by default)
79+
self.devices = []
6580
if "network_id" in config and "host_id" in config and config["network_id"] and (config["host_id"]["start"] <= config["host_id"]["end"]):
6681
network_id = config["network_id"]
6782
host_id_range = range(config["host_id"]["start"], config["host_id"]["end"]+1)
@@ -76,13 +91,16 @@ def on_mount(self) -> None:
7691
d = Device(str(ip_addr), '5555')
7792
self.devices.append(d)
7893

94+
#Init offset logger for all devices if not in single session mode
95+
if not self.single_session_mode:
96+
self.offset_logger = {dev: None for dev in ip_list}
97+
7998
data = [(None, d.ip_addr, None, None, None, None, None, None, None, None, None, None, None, None, None) for d in self.devices]
8099
self.row_keys = table.add_rows(data)
81100
table.styles.scroll_x = "scroll_x"
82-
83101
#Splitting columns into two batches with different update timers. This setting could be configured further with more batches or a single batch.
84-
self.set_interval(3, self.batch_col_update1)
85-
self.set_interval(5, self.batch_col_update2)
102+
self.set_interval(config["update_times"]["batch1"], self.batch_col_update1)
103+
self.set_interval(config["update_times"]["batch2"], self.batch_col_update2)
86104

87105
self.table_app_start_time = datetime.now()
88106

@@ -451,12 +469,12 @@ def start_recording(self, ip_addr):
451469
time.sleep(3)
452470

453471
try:
454-
logger.info(f'Start recording on {ip_addr}')
472+
self.logger.info(f'Start recording on {ip_addr}')
455473
res = requests.post(f"http://{ip_addr}:8080/api/recording:start", timeout=2).json()
456474
time.sleep(0.1)
457475
self.lock_phone(ip_addr)
458476
except Exception as e:
459-
logger.error(f'{ip_addr}, {e}')
477+
self.logger.error(f'{ip_addr}, {e}')
460478
pass
461479

462480
return res
@@ -472,10 +490,9 @@ def stop_and_save_recording(self, ip_addr):
472490
"""
473491
res = None
474492
try:
475-
logger.info(f'Stop and save recording on {ip_addr}')
476493
res = requests.post(f"http://{ip_addr}:8080/api/recording:stop_and_save", timeout=2).json()
477494
except Exception as e:
478-
logger.error(f'{ip_addr}, {e}')
495+
self.logger.error(f'{ip_addr}, {e}')
479496
pass
480497
return res
481498

@@ -490,10 +507,9 @@ def stop_and_discard_recording(self, ip_addr):
490507
"""
491508
res = None
492509
try:
493-
logger.info(f'Stop and discard recording on {ip_addr}')
494510
res = requests.post(f"http://{ip_addr}:8080/api/recording:cancel", timeout=2).json()
495511
except Exception as e:
496-
logger.error(f'{ip_addr}, {e}')
512+
self.logger.error(f'{ip_addr}, {e}')
497513
print(e)
498514
pass
499515
return res
@@ -521,6 +537,29 @@ def on_input_submitted(self, event: Input.Submitted) -> None:
521537
# Clear box
522538
input_box.value = ""
523539

540+
def stop_recording_offsets(self, device_list):
541+
"""
542+
Stop logging offsets for the specified devices.
543+
Args:
544+
devices (list): List of device IP addresses to stop logging offsets for.
545+
"""
546+
try:
547+
if self.single_session_mode:
548+
if self.offset_logger:
549+
self.offset_logger.stop_logging()
550+
self.offset_logger = None
551+
self.logger.info("Stopped offset logging for all devices.")
552+
else:
553+
for dev in device_list:
554+
if self.offset_logger[dev]:
555+
self.offset_logger[dev].stop_logging()
556+
self.offset_logger[dev] = None
557+
self.logger.info("Stopped offset logging for devices: {}".format(device_list))
558+
except Exception as e:
559+
self.logger.error(f'Error stopping offset logging: {e}')
560+
print(e)
561+
pass
562+
524563
#Defining actions
525564
@work(exclusive=True, thread=True)
526565
async def action_recording_start(self) -> None:
@@ -531,13 +570,22 @@ async def action_recording_start(self) -> None:
531570
"""
532571
table = self.query_one(SelectableRowsDataTable)
533572
selected_devices = [row.data[1] for row in table.selected_rows]
534-
logger.info("Selected devices ({}): {}".format(len(selected_devices), selected_devices))
573+
self.logger.info("Selected devices ({}): {}".format(len(selected_devices), selected_devices))
535574
print("STARTING REC on selected devices")
536575

537-
if not self.offset_logger:
538-
self.offset_logger = OffsetLogger(selected_devices, log_dir=config["paths"]["logs"])
539-
logger.info(f"Starting Offset logger at {self.offset_logger.log_file}")
540-
self.offset_logger.log_offsets()
576+
if self.single_session_mode:
577+
if not self.offset_logger:
578+
self.offset_logger = OffsetLogger(selected_devices, log_dir=self.session_dir, log_interval=config["logs"]["interval"])
579+
self.logger.info(f"Starting Offset logger at {self.offset_logger.log_file}")
580+
self.offset_logger.log_offsets()
581+
else:
582+
for dev in selected_devices:
583+
if not self.offset_logger[dev]:
584+
self.offset_logger[dev] = OffsetLogger([dev], log_dir=os.path.join(self.session_dir, str(dev)), log_interval=config["logs"]["interval"])
585+
self.logger.info(f"Starting Offset logger at {self.offset_logger[dev].log_file} for device: {dev}")
586+
self.offset_logger[dev].log_offsets()
587+
588+
#Start recording on all devices independently
541589
for d in selected_devices:
542590
t = threading.Thread(target=self.start_recording, args=(d,), daemon=True)
543591
t.start()
@@ -554,10 +602,10 @@ async def action_recording_stop_and_save(self) -> None:
554602
"""
555603
table = self.query_one(SelectableRowsDataTable)
556604
selected_devices = [row.data[1] for row in table.selected_rows]
557-
logger.info("Selected devices ({}): {}".format(len(selected_devices), selected_devices))
558-
print("STOPPING REC on selected devices")
559-
560-
logger.info("Stopping recording on devices")
605+
print("STOPPING REC on selected device(s): ", selected_devices)
606+
self.logger.info(f"Stopping recording on {len(selected_devices)} device(s): {selected_devices}")
607+
# Stop offset logging if it was started
608+
self.stop_recording_offsets(selected_devices)
561609
for d in selected_devices:
562610
t = threading.Thread(target=self.stop_and_save_recording, args=(d,), daemon=True)
563611
t.start()
@@ -571,10 +619,10 @@ async def action_recording_stop_and_discard(self) -> None:
571619
"""
572620
table = self.query_one(SelectableRowsDataTable)
573621
selected_devices = [row.data[1] for row in table.selected_rows]
574-
logger.info("Selected devices ({}): {}".format(len(selected_devices), selected_devices))
575622
print("DISCARDING REC on selected devices")
576-
577-
logger.info("Discarding recording on devices")
623+
# Stop offset logging if it was started
624+
self.stop_recording_offsets(selected_devices)
625+
self.logger.info(f"Discarding recording on {len(selected_devices)} device(s): {selected_devices}")
578626
for d in selected_devices:
579627
t = threading.Thread(target=self.stop_and_discard_recording, args=(d,), daemon=True)
580628
t.start()
@@ -586,9 +634,9 @@ async def action_restart_app_on_devices(self) -> None:
586634
This method retrieves the selected devices from the UI and restarts
587635
the Neon Companion application on each one.
588636
"""
589-
logger.info('action_restart_app_on_devices triggered!')
637+
self.logger.info('action_restart_app_on_devices triggered!')
590638
if self.restart_app_in_progress:
591-
logger.info('Another restart progress is already in progress, nothing to do...')
639+
self.logger.info('Another restart progress is already in progress, nothing to do...')
592640
return
593641
print("RESTARTING APP on selected devices")
594642

@@ -597,14 +645,14 @@ async def action_restart_app_on_devices(self) -> None:
597645

598646
table = self.query_one(SelectableRowsDataTable)
599647
selected_device_ip_addrs = [row.data[1] for row in table.selected_rows]
600-
logger.info("Selected devices ({}): {}".format(len(selected_device_ip_addrs), selected_device_ip_addrs))
648+
self.logger.info("Selected devices ({}): {}".format(len(selected_device_ip_addrs), selected_device_ip_addrs))
601649

602650
def f(ip_addr):
603-
logger.info(f'Restarting app on {ip_addr}...')
651+
self.logger.info(f'Restarting app on {ip_addr}...')
604652
adb_wrapper = AdbWrapper(ip_addr)
605653
adb_wrapper.stop_neon_companion_app()
606654
adb_wrapper.start_neon_companion_app()
607-
logger.info(f'Restarting app on {ip_addr} has finished!')
655+
self.logger.info(f'Restarting app on {ip_addr} has finished!')
608656

609657
tasks = []
610658
for ip_addr in selected_device_ip_addrs:
@@ -614,10 +662,10 @@ def f(ip_addr):
614662

615663
for t in tasks:
616664
t.join()
617-
logger.info(f'Restarting apps has finished!')
665+
self.logger.info(f'Restarting apps has finished!')
618666
finally:
619667
self.restart_app_in_progress = False
620-
logger.info(f'self.restart_app_in_progress = False')
668+
self.logger.info(f'self.restart_app_in_progress = False')
621669

622670
@work(exclusive=True, thread=True)
623671
async def action_reconnect_adb(self) -> None:
@@ -628,22 +676,26 @@ async def action_reconnect_adb(self) -> None:
628676
print("RESTARTING ADB on selected devices!")
629677
table = self.query_one(SelectableRowsDataTable)
630678
selected_device_ip_addrs = [row.data[1] for row in table.selected_rows]
631-
logger.info("Selected devices ({}): {}".format(len(selected_device_ip_addrs), selected_device_ip_addrs))
679+
self.logger.info("Selected devices ({}): {}".format(len(selected_device_ip_addrs), selected_device_ip_addrs))
680+
681+
def run_adb_cmd(ip_addr):
682+
"""Runs the adb connect command for a single device."""
683+
self.logger.info(f"Restarting adb on {ip_addr}...")
684+
res = subprocess.run(f"adb connect {ip_addr}:5555", shell=True, capture_output=True, text=True)
685+
self.logger.info(res.stdout.strip())
632686

633687
for ip_addr in selected_device_ip_addrs:
634-
logger.info(f'Restarting adb on {ip_addr}...')
635-
res = subprocess.run("adb connect {}:5555".format(ip_addr), shell=True, capture_output=True, text=True).stdout
636-
logger.info(res.strip())
688+
t = threading.Thread(target=run_adb_cmd, args=(ip_addr,), daemon=True)
689+
t.start()
637690

638-
print('FINISHED restarting adb on all devices!')
691+
print('FINISHED dispatching adb restart threads!')
639692

640693
@work(exclusive=True, thread=True)
641-
def action_stop_offsets(self) -> None:
642-
""""""
643-
if self.offset_logger:
644-
logger.info("Stopping Offset logger")
645-
self.offset_logger.stop_logging()
646-
self.offset_logger = None
694+
def action_stop_all_offsets(self) -> None:
695+
"""
696+
Stops all ongoing offset logging activities.
697+
"""
698+
self.stop_recording_offsets(self.devices)
647699

648700
def action_toggle_dark(self) -> None:
649701
"""An action to toggle dark mode."""
@@ -659,7 +711,7 @@ def action_toggle_dark(self) -> None:
659711
description="Save Recording"),
660712
Binding(key="u", action="recording_stop_and_discard",
661713
description="Cancel Recording"),
662-
Binding(key="o", action="stop_offsets", description="Stop offsets logging"),
714+
Binding(key="o", action="stop_all_offsets", description="Stop offsets logging on all devices"),
663715
Binding(key="t", action="restart_app_on_devices", description="Restart App"),
664716
Binding(key="a", action="reconnect_adb", description="Reconnect adb"),
665717
Binding(key="d", action="toggle_dark", description="Toggle dark mode"),

0 commit comments

Comments
 (0)