Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
1db8208
Audio tweaks
Mar 5, 2021
f8b0235
Remove s in https and wss for testing
Mar 24, 2025
926454a
Add ffmpeg-ivs.py file
alexstrangeparts Mar 24, 2025
50a490f
Change ffmpeg command line format
alexstrangeparts Mar 24, 2025
18df543
bigass commit of things
Jun 12, 2025
bf537fc
remove temp files
Jun 12, 2025
6611657
add enable config for gps
alexstrangeparts Jun 13, 2025
87373d5
move gps info log
alexstrangeparts Jun 14, 2025
b43f63b
fix gps enable boolean check
alexstrangeparts Jun 14, 2025
5bd8bed
Fix ffmpeg-ivc not using config mic number
alexstrangeparts Jun 14, 2025
1decc43
fix typo in ffmpeg code
alexstrangeparts Jun 14, 2025
af4dbe9
add hw: prefix to mic hardware number
alexstrangeparts Jun 14, 2025
cef8d37
Add pre ffmpeg command support for libcamera use
alexstrangeparts Jun 14, 2025
72530f6
fix incorrect section for pre_ffmpeg config
alexstrangeparts Jun 14, 2025
bb1f624
add small delay to ffmpeg restart
alexstrangeparts Jun 14, 2025
2e971eb
remove redundant ffmpeg commands to prevent issues
alexstrangeparts Jun 14, 2025
a4aa2e0
Add configuration option for thread queue size
alexstrangeparts Jun 15, 2025
fbed7c6
update config defaults
alexstrangeparts Jun 15, 2025
ba587f1
updated sample config
alexstrangeparts Jun 18, 2025
90f8fee
update requirements
alexstrangeparts Jun 18, 2025
ff793ee
use sample rate configuration
alexstrangeparts Jun 18, 2025
d7c9b37
add audio thread queue size
alexstrangeparts Jun 18, 2025
2b7031c
Add dummy sound card on startup
alexstrangeparts Jun 18, 2025
deddb48
Update start script to go to correct directory
alexstrangeparts Jun 18, 2025
7c1ec10
Update command ordering for audio
sahko123 Jun 27, 2025
dae79a8
Ensure audio devices are correctly initialised
sahko123 Jun 28, 2025
9d28946
add support for dual streaming with ffmpeg
sahko123 Jun 28, 2025
595169b
fix video-ivc config check
sahko123 Jun 28, 2025
c8f97ce
added quote marks to ffmpeg output stream
sahko123 Jun 28, 2025
00b12b5
remove erroneous space
sahko123 Jun 28, 2025
a4c7227
update robot connect endpoint to have trailing slash
sahko123 Jul 12, 2025
a837ec8
add config flag for streaming to both ivs and obs or either or
sahko123 Jul 12, 2025
f647b6e
replace servers 1 and 2 to servers for ivs and obs separately
sahko123 Jul 13, 2025
e49b57a
remo chat_test and replace with chat
sahko123 Jul 14, 2025
67cda17
Enable geofencing
alexstrangeparts Nov 25, 2025
1e6dea4
report inactivity to server after a length of time has passed without…
alexstrangeparts Nov 28, 2025
fcdc551
Queue tts messages to prevent them overlapping each other
alexstrangeparts Nov 25, 2025
b53c42f
Merge pull request #1 from strangeparts/dev-alex-queue-tts-messages
alexstrangeparts Feb 21, 2026
27c4859
rename inactive_time to more descriptive last_active_time
alexstrangeparts Feb 21, 2026
be0f49f
Merge branch 'dev-alex-different-site' into dev-alex-report-inactivity
alexstrangeparts Feb 21, 2026
851b961
Merge pull request #2 from strangeparts/dev-alex-report-inactivity
alexstrangeparts Feb 21, 2026
b7521b6
Fix time.time.now() to time.time()
alexstrangeparts Feb 23, 2026
5c6f84c
Move play call outside with block in google_cloud TTS
alexstrangeparts Feb 23, 2026
f1e112e
Add geofence enforcement with moderator bypass
alexstrangeparts Feb 23, 2026
1c14265
Send TTS_FINISHED directly from thread instead of polling
alexstrangeparts Feb 23, 2026
0e352de
Merge pull request #3 from strangeparts/dev-alex-fix-time-call
alexstrangeparts Feb 23, 2026
39af801
Merge pull request #6 from strangeparts/dev-alex-fix-tts-file-handle
alexstrangeparts Feb 23, 2026
b6362f2
Merge pull request #8 from strangeparts/dev-alex-tts-finish-from-thread
alexstrangeparts Feb 23, 2026
97af1b3
Rename is_breaking_geofence to is_within_geofence for clarity
alexstrangeparts Feb 23, 2026
ab8c41c
Merge pull request #4 from strangeparts/dev-alex-geofence-enforcement
alexstrangeparts Feb 23, 2026
7c21b99
Add geofence breach/clear reporting to server
alexstrangeparts Feb 23, 2026
85d8ad4
Fix geofence blocking all commands after initial breach
alexstrangeparts Feb 23, 2026
efa79c0
Fix main loop setting geofence_was_breached without reporting
alexstrangeparts Feb 23, 2026
5270dec
Fix: add comment explaining post-breach command passthrough
alexstrangeparts Feb 24, 2026
46892ef
Fix: report last executed f/b direction on geofence breach
alexstrangeparts Feb 24, 2026
1b27fe3
Merge pull request #9 from strangeparts/dev-alex-geofence-alert
alexstrangeparts Feb 24, 2026
e7ad8d4
Clean up TTS temp files and log playback errors
alexstrangeparts Feb 26, 2026
46ea8fb
Fix ffmpeg restart loop in ffmpeg-ivc
alexstrangeparts Feb 26, 2026
e635249
Wire up audio_input_options to ffmpeg command template
alexstrangeparts Feb 26, 2026
166b32d
Send TTS_FINISHED on startup to clear stale server lock
alexstrangeparts Feb 26, 2026
bd485e2
Fix config file handle leak — f.close missing parentheses
alexstrangeparts Feb 26, 2026
35ba4a0
Fix sendChatMessge typo in ffmpeg-ivc.py
alexstrangeparts Feb 26, 2026
8451f29
Fix: use pkill -x instead of -f to avoid killing unrelated processes
alexstrangeparts Feb 27, 2026
fb19ce3
Fix: poll for video device release instead of fixed sleep
alexstrangeparts Feb 27, 2026
2ff89e4
Fix: wrap file write in try/finally to clean up temp file on write fa…
alexstrangeparts Feb 27, 2026
ab56579
Fix: use context manager for config file write
alexstrangeparts Feb 27, 2026
f68504b
Merge pull request #10 from strangeparts/dev-alex-ffmpeg-improvements
alexstrangeparts Feb 27, 2026
4483094
Merge pull request #11 from strangeparts/dev-alex-fix-tts-cleanup
alexstrangeparts Feb 27, 2026
0245323
Merge pull request #12 from strangeparts/dev-alex-fix-config-write
alexstrangeparts Feb 27, 2026
8e843cb
Fix Python 2 syntax error: remove non-ASCII em dash from comment
alexstrangeparts Feb 27, 2026
8258e76
Merge pull request #13 from strangeparts/dev-alex-remove-m-dash
alexstrangeparts Feb 27, 2026
0b25324
Allow video capture to start when authenticated even without internet…
alexstrangeparts Mar 23, 2026
efe2ac0
Fix: remove dead if-condition guarding video capture start
alexstrangeparts Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 103 additions & 11 deletions controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
import logging.handlers
import json
import atexit
import gps
import subprocess
import threading
from ast import literal_eval

last_active_time = 0

if (sys.version_info > (3, 0)):
import importlib
Expand Down Expand Up @@ -111,9 +117,8 @@ def write(self, config_file):
os.rename(config_file, config_file+'.bak')

# write out the updated config file
f = open(config_file, 'w')
f.writelines(lines)
f.close
with open(config_file, 'w') as f:
f.writelines(lines)

log.info("Config file saved.")

Expand Down Expand Up @@ -219,8 +224,8 @@ def str2bool(v):
# Functions

def handle_message(ws, message):
global last_active_time
log.debug(message)

try:
messageData = json.loads(message)
except:
Expand All @@ -234,15 +239,42 @@ def handle_message(ws, message):
data = messageData["d"]

if event == "BUTTON_COMMAND":
last_active_time = time.time()
log.debug("Received Button Command")
data['moderator'] = messageData.get('moderator', False)
on_handle_command(data)
# handle_command(data)

elif event == "MUTE_MIC":
process = subprocess.Popen(
["pactl", "set-source-mute", "@DEFAULT_SOURCE@", "1"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout, stderr = process.communicate()

log.debug("STDOUT: %s", stdout)
log.debug("STDERR: %s", stderr)

elif event == "UNMUTE_MIC":
process = subprocess.Popen(
["pactl", "set-source-mute", "@DEFAULT_SOURCE@", "0"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout, stderr = process.communicate()

log.debug("STDOUT: %s", stdout)
log.debug("STDERR: %s", stderr)


elif event == "MESSAGE_RECEIVED":
if data['channel_id'] == networking.channel_id:
if data['type'] != "robot":
if data['type'] == 'message':
on_handle_chat_message(data)

elif event == "ROBOT_VALIDATED":
last_active_time = time.time()
networking.handleConnectChatChannel(data["host"])

else:
Expand All @@ -267,16 +299,32 @@ def handle_chat_message(args):

def handle_command(args):
global handlingCommand
global geofence_was_breached
global last_move_direction
handlingCommand = True

# catch move commands that happen before the controller has fully
# loaded and set a move handler.
if move_handler == None:
return

if gps_is_enabled() and not args.get('moderator', False):
# Only block and report on the first breach. After that, let commands
# through - the server decides which directions to allow/block.
if not gps.is_within_geofence() and not geofence_was_breached:
log.info('Movement blocked by geofence, last direction: %s' % last_move_direction)
networking.reportGeofenceBreach(last_move_direction)
geofence_was_breached = True
handlingCommand = False
return

log.debug('got command : %s', args)
move_handler(args)

command = args.get('button', {}).get('command', '')
if command in ('f', 'b'):
last_move_direction = command

handlingCommand = False


Expand All @@ -288,15 +336,28 @@ def on_handle_command(*args):
thread.start_new_thread(handle_command, args)

def on_handle_chat_message(*args):
if chat_module == None:
thread.start_new_thread(handle_chat_message, args)
else:
thread.start_new_thread(chat_module.handle_chat, args)
if chat_module is None:
target = handle_chat_message
else:
target = chat_module.handle_chat

def _tts_then_finish():
try:
target(*args)
except Exception as e:
log.error('TTS error: %s', e)
networking.declareFinishedTTS()

t = threading.Thread(target=_tts_then_finish)
t.daemon = True
t.start()

def restart_controller(command, args):
if extended_command.is_authed(args['sender']) == 2: # Owner
terminate.acquire()

def gps_is_enabled():
return robot_config.getboolean('gps', 'enable_gps')

# TODO : This really doesn't belong here, should probably be in start script.
# watch dog timer
Expand Down Expand Up @@ -387,13 +448,44 @@ def restart_controller(command, args):

else:
log.warning("Unable to find chat_custom.py")




geofence_was_breached = False
last_move_direction = ''

if robot_config.has_section('gps') and robot_config.getboolean('gps', 'enable_gps'):
geofence_points = literal_eval(robot_config.get('gps', 'geofence_array'))
log.info("Starting GPS thread")
gps.start_gps_thread()
log.debug(geofence_points)
for point in geofence_points:
log.debug(point)
gps.set_geofence(geofence_points)
else:
log.critical('Please configure gps settings')


atexit.register(log.debug, "Attempting to clean up and exit nicely")
if not test_mode:
log.critical('RemoTV Controller Started')
while not terminate.locked():
while not terminate.locked(): # Main control loop. This handles anythign that involves timing, everything else is asynchronous
time.sleep(1)
watchdog.watch()

if last_active_time + robot_config.getint('robot', 'inactivity_timeout') < time.time():
networking.reportInactivity()

if robot_config.getboolean('gps', 'enable_gps'):
lat, lon = gps.get_position() # Get the latest position
if lat is not None and lon is not None:
if lat == 0 and lon == 0:
log.debug("Waiting for GPS fix...")
else:
inside = gps.is_within_geofence()
if geofence_was_breached and inside:
networking.reportGeofenceClear()
geofence_was_breached = False

log.critical('RemoTV Controller Exiting')
else:
Expand Down
60 changes: 38 additions & 22 deletions controller.sample.conf
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,28 @@ type=none
turn_delay=0.4
straight_delay=0.5

inactivity_timeout=30 # Time after which, the robot reports inactivity (in seconds)

[gps]
enable_gps = false
geofence_array = []

[camera]
# Disable video
no_camera=false

# Disable mic
no_mic=false


# Specify the audio / video encoder you are using here. Currently ffmpeg,
# ffmpeg-arecord and none. Note: Only robots with Raspberry Pi's or other Linux
# based controllers should use ffmpeg-arecord. All others should use ffmpeg or
# none.
type=ffmpeg-arecord
type=ffmpeg-ivc

# X and Y resolution to capture video at
#x_res = 640
#y_res = 480
x_res = 768
y_res = 432
#x_res = 1280
#y_res = 720
x_res = 1280
y_res = 720

# Video device
camera_device = /dev/video0
Expand All @@ -60,7 +61,14 @@ mic_device=

[ffmpeg]
# Combined ffmpeg options for audio and video
# stream_output = ivs
# stream_output = obs
# stream_output = both # IVS and OBS
stream_output_destinations = ivs

pre_ffmpeg =
ffmpeg_location = /usr/bin/ffmpeg
thread_queue_size = 1024
# Windows path example
#ffmpeg_location = c://ffmpeg//bin//ffmpeg.exe

Expand All @@ -69,22 +77,22 @@ v4l2-ctl_location=/usr/bin/v4l2-ctl

# Audio codec ffmpeg should use, only mp2 is supported but twolame will work
# when compiled in to ffmpeg.
audio_codec = mp2
audio_codec = aac

# Audio channels, 1 for mono 2 for stereo.
audio_channels = 1

# Bitrate for the audio stream in kilobytes
audio_bitrate = 32
audio_bitrate = 320

# Sample rate fot the audio stream in hertz
audio_sample_rate = 44100

# Video codec ffmpeg should use. Currently only mpeg1video is supported.
video_codec = mpeg1video
video_codec = h264

# Bitrate for the video stream in kilobytes
video_bitrate = 350
video_bitrate = 1500

# Video filter options. To rotate the video 180 degree, uncomment this line
#video_filter = transpose=2,transpose=2
Expand Down Expand Up @@ -118,7 +126,7 @@ arecord_format=S16_LE
[tts]
# Specify the TTS engine you are using here. Current valid types are espeak,
# festival and none
type=espeak
type=none

# TTS volume
tts_volume=80
Expand All @@ -131,7 +139,7 @@ anon_tts=true
filter_url_tts=true

# Enable extended chat commands
ext_chat=true
ext_chat=False

# ALSA HW number for your playback device
# For Tellys this is 2.
Expand Down Expand Up @@ -353,8 +361,8 @@ topic = LR/command
# Name of the log file to be written to
log_file=controller.log
# log levels in order of importance are CRITICAL, ERROR, WARNING, INFO, DEBUG
file_level=WARNING
console_level=ERROR
file_level=DEBUG
console_level=DEBUG
# log size is in bytes
max_size=100000
# Number of old log files to hang onto.
Expand All @@ -363,19 +371,27 @@ num_backup=2
# This is mostly stuff you probably shouldn't be touching
[misc]
# host server to connect to
server = remo.tv
server = robots.electricranch.co
# API version is the version of the API.
api_version = dev
# video server to connect to so you can use local websocket and remote video server
video_server = remo.tv:1567
video_format_ivs =
video_server_ivs =
video_stream_key_ivs =

video_format_obs =
video_server_obs =
video_stream_key_obs =


# Enable the controller to look for custom handler code
custom_hardware = True
custom_hardware = False
# Enable the controller to look for custom TTS handler code
custom_tts = True
custom_tts = False
# Enable the controller to look for custom chat handler code
custom_chat = True
custom_chat = False
# Enable the controller to look for custom video handler code
custom_video = True
custom_video = False
# Enable the watchdog timer, if you are not using a raspberry pi, you won't want
# this.
watchdog = True
Expand Down
Loading