BLE gatttool interactive shell script - bash

I established a connection with a BLE device using gatttool. First I connected to the device with sudo gatttool -t random -b FF:3C:8F:22:C9:C8 -I and connect. After that I read the value of specific characteristic with char-read-uuid 2d30c082-f39f-4ce6-923f-3484ea480596.
What I want to do is to automate the whole process and put the latter command (querying for value) in the loop, ideally saving each value (appending) to a text file. I tried something like
sudo gatttool -t random -b FF:3C:8F:22:C9:C8 -I <<EOF
connect
while[ 1 ]; do
char-read-uuid 2d30c082-f39f-4ce6-923f-3484ea480596 > output.txt
done
exit 1
EOF
but it does not help, since I am not even able to connect to the device (ideally there should be some delay between the first and the second command). Also after connecting, an interactive mode is enabled and the shell commands do not work there. I'd appreciate any clues on how to tackle this issue.

If gattool writes prompts to stdout (and doesn't suppress them given non-TTY file descriptors), consider something like:
#!/usr/bin/env bash
case $BASH_VERSION in ''|[123].*|4.0.*) echo "ERROR: bash 4.1 or newer required" >&2; exit 1;; esac
exec {output_fd}>output.txt
prompt_re='[>] '
capture_re='^handle:.*value:.*$'
wait_for_prompt() {
IFS= read -r line || return
while ! [[ $line =~ $prompt_re ]]; do
[[ $line =~ $capture_re ]] && printf '%s\n' "$line" >&$output_fd
IFS= read -r line || return
done
}
wait_for_prompt
echo connect
while wait_for_prompt; do
echo "char-read-uuid 2d30c082-f39f-4ce6-923f-3484ea480596"
done
...saved as yourscript, and invoked using socat as:
socat 'SYSTEM:sudo gatttool -t random -b FF:3C:8F:22:C9:C8 -I 2>&1' 'EXEC:./yourscript'
(assuming that sudo is configured to work without a TTY; otherwise, you might move it to be sudo socat).

Indeed, pexpect works fine here. You can find my solution below. The code reads the value of the specific UUID, which contains IMU readings (floats).
import pexpect
import struct
import time
import sys
IMU_MAC_ADDRESS = "FF:3C:8F:22:C9:C8"
UUID_DATA = "2d30c082-f39f-4ce6-923f-3484ea480596"
if __name__ == '__main__':
gatt = pexpect.spawn("gatttool -t random -b " + IMU_MAC_ADDRESS + " -I")
gatt.sendline("connect")
gatt.expect("Connection successful")
while(True):
gatt.sendline("char-read-uuid " + UUID_DATA)
gatt.expect("handle: 0x0011 value: ")
gatt.expect(" \r\n")
data = (gatt.before).decode('UTF-8').replace(" ", "").decode('hex')
print(struct.unpack('f', data)[0]

Related

How to make one loop out of five in bash?

I have a small script for Mac where I'm adding printers. It works fine but I think I could make it simpler or at least it would be interesting to know a different solution.
while IFS= read -r line;
do
if [[ $line == *"Printer_E1"* ]]
then
if [[ "$FIND_PRINTERS" =~ "$PRINTER_E1_IP" ]];
then
echo "found printer e1"
else
echo "adding printer e1"
"$LPADMIN" -p "$PRINTER_E1_IP" -v "lpd://$PRINTER_E1_IP" -L "$PRINTER_E1_LOCATION" -P "$PRINTER_E1_PPD" -E -o printer-is-shared=false -D "$PRINTER_E1_NAME"
echo "adding printer e1 done"
fi
fi
done <<< "$AD_GROUPS"
The content of $AD_GROUPS is:
Printer_E0
Printer_E1
Printer_E2
Printer_E3
Printer_E4
Printer_Strasse
Printer_Wien
I have such a loop for 5 printers, so 5 times that just with different variables.
How could I do that with one loop? (or how can I make that different or simpler)?
Something like this:
while IFS= read -r printer; do
[[ "$FIND_PRINTERS" =~ "${printer}_IP" ]] && \
echo "Found ${printer}" && continue
echo "Adding ${printer}..."
"$LPADMIN" -p "${printer}_IP" \
-v "lpd://${printer}_IP" \
-L "${printer}_LOCATION" \
-P "${printer}_PPD" -E -o printer-is-shared=false \
-D "${printer}_NAME" \
&& echo "Done"
done <<< "$AD_GROUPS"
I assume your variable FIND_PRINTERS has some printers you want to skip, that you have already set the parameters (IP, LOCATION etc) related to each printer.
We use the variable inside double quotes into there, so it expands to what you want for the various commands. Also I have simplified the if condition then command to condition && command and also continue moves to next iteration.

Asynchronously reading and processing user input without delay or disruption of the main task

I'm experiencing a weird niggly issue with bash:
I have the following for loop:
for i in {1..9999}
do
printf '%s%04d' "(311) 399-" "$i"
read -r -s -t 6 input
#echo "x $input"
if [ "$input" == "q" ] || [ "$input" == "Q" ] || [ "$input" == "x" ] || [ "$input" == "X" ] || [ "$input" == "p" ] || [ "$input" == "p" ]
then
#echo "1"
break
else
#echo "2"
printf '\n'
fi
done
The loop itself works as expected. It prints out text like this:
(311) 399-0001
(311) 399-0002
(311) 399-0003
The problem is that the loop can't be exited. The idea was to have this continue to a maximum of 9999, but if the ENTER key was pressed, break the loop immediately. I was having issues getting IFS to work with "" being equated to ENTER, so I opted to choose a few letters a used wishing to exit the loop would probably press, such as Q, X, and P.
However, the loop would never exit when keys were pressed.
To debug, I added in the lines that are commented out above. To my surprise, nothing was printing out and it was always choosing route "2" - input was not being registered.
I've tried playing with the formatting of the read statement. When I change read -r -s -t 6 "" input to read -r -s -t 6 "" input, I get a strange error:
(311) 399-9998/home/com/wopr.sh: line 923: read: `': not a valid identifier
x
2
(311) 399-9999/home/com/wopr.sh: line 923: read: `': not a valid identifier
x
2
I tried removing the -r part as well and the -p option but nothing has changed.
I have hundreds of other read statements in this and other scripts and this is the first issue I am having with any of them. What's causing the input variable to always be null?
Workaround:
I did figure out a workaround for this. A bit unconventional, but it does work, allowing ENTER to be pressed but not other characters. No input also does not equate to an ENTER:
for i in {1..9999}
do
printf '%s%04d' "(311) 399-" "$i"
time1=`date +%s`
read -t 6 -s -r -p "" input
time2=`date +%s`
dif=$((time2-time1))
if [ "$dif" == "6" ] && [ "$input" == "" ]
then
printf '\n'
else
break
fi
done
The way you've written it, read expects a complete line:
Press Return to continue
Press qReturn to quit
When you just press q without Return, then you're not inputting a line so the variable is empty.
To be able to press a single key without Return, use read -n 1
Implementation with Asynchronous reading of user quit key
and inter-task messaging.
#InterLinked, Looked like Bash robocalling, so here it is:
#!/usr/bin/env bash
# Handle exit to cleanly dispose of resources
exit_trap() {
# If there is a PID of the input_task
if [[ -n ${input_pid:-} ]]; then
# SIGnal the input_task to TERMinate
kill 2>/dev/null -s TERM "${input_pid}" || true
wait "${input_pid}" # Be sure it has stopped
unset input_pid
fi
# Close/Free the message File Descriptor
[[ -n ${msg_fd} ]] && exec {msg_fd}>&-
# Purge the Temporary FIFO Directory
[[ -n ${fifo_dir} && -d ${fifo_dir} ]] \
&& { rm -fr -- "${fifo_dir}" >/dev/null 2>&1 || true; }
}
# Install an exit handler for cleanup before exit
trap exit_trap EXIT ERR
# Create the Temporary FIFO Directory
# Create the FIFO device in it with name msg_fifo
# Assign the named File Descriptor msg_fd to the FIFO msg_fifo
fifo_dir="$(mktemp --directory)" \
&& msg_fifo="${fifo_dir}/msg_fifo" \
&& mkfifo "${msg_fifo}" \
&& exec {msg_fd}<>"${msg_fifo}"
# Run in the background to get user input, and eventually SIGnal an IO
# to the main loop, with a quit message that is just the integer value 1.
input_task() {
local -- input
while :; do
read -r -s -N 1 input # Read exactly -N 1 character, -r raw, -s silent
case "${input}" in
[qQxXpP]) # POSIX Character class matching q, Q, x, X, p, P
# Output the integer value 1 as the message content,
# to the Named File Descriptor msg_fd
echo >&${msg_fd} 1
# SIGnal IO to the foreground task IO-trap handler,
# that there is a message to read
kill -s IO $$
;;
esac
done
}
# msg is the Message-Box variable. It Needs to be global,
# so it can be populated by the IO signal handler.
typeset -- msg=''
# Launch the input_task in the background, with the current stdio input &1.
input_task <&1 &
# Save the process ID of the background-running input_task.
typeset -- input_pid="$!"
# This is the IO message handler subscribed to the IO trap signal:
# It reads -u Named File Descriptor msg_fd, -r raw into the msg Message-Box variable
trap 'read -u ${msg_fd} -r msg' IO
# Now that the input_task is running in the background, and there is
# an IO trap handler to read the messages from the input_task...
# Here starts the main stuffs, not bothering with waiting, decoding,
# retrying user inputs, as all of it is done asynchronously by
# the background input_task.
typeset -i \
area_code=311 \
switch_code=399 \
call_index_min=1 \
call_index_max=9999 \
call_freq=6
typeset -i \
to_call \
remaining_time
to_call=$((1 + call_index_max - call_index_min))
remaining_time=$((call_freq * to_call - 3600))
printf $'Calling a total of %d number(s), one per %ds.\n' \
"${to_call}" \
"${call_freq}"
printf $'Total time to complete calls: %()T.\n\n' "${remaining_time}"
while read -r line_number; do
typeset -- phone_number='(area) switch-line'
printf -v phone_number '(%03d) %03d-%04d' \
"${area_code}" \
"${switch_code}" \
"${line_number}"
printf $'%()T, %d to go, Calling: %s\n' \
"${remaining_time}" \
"${to_call}" \
"${phone_number}"
((remaining_time -= call_freq, to_call--))
# Wait call_freq seconds in the background,
sleep "${call_freq}" &
# to allow interruptions of the sleep,
# when there is a message signaled by the IO trap
# so it can quit immediately on receiving msg=1
wait $!
((msg)) && {
echo $"ABORT"
break
} # msg -eq 1, so break out of the loop
done < <(
# Generate a scrambled list of line numbers
sort \
--random-sort \
< <(
seq \
"${call_index_min}" "${call_index_max}"
)
)
Sample output:
Calling a total of 9999 number(s), one per 6s.
Total time to complete calls: 16:39:54.
16:39:54, 9999 to go, Calling: (311) 399-4409
16:39:48, 9998 to go, Calling: (311) 399-2174
16:39:42, 9997 to go, Calling: (311) 399-3840
16:39:36, 9996 to go, Calling: (311) 399-4583
ABORT

Need way to approve bash cmd exec before running w/o affecting echo return

Currently trying to write bash that will do the following.
check if curl is installed
print out "which curl" before running it so that the user is able to opt in/out of running something they consider unsafe.
Use case is when you download a big script from github and you want to have more control over what it is doing. Also to be more aware of how it works.
I am not sure how to include this opt in/out code without messing up the "return" echo. Maybe the answer is to use something different that the read -n 1 -s -r -p code. I like that solution because it allows hitting any key to continue.
To be clear. If I check for YES/NO later on, it is messed up because it will contain the character used to continue by pressing any key. In my output example the space bar was hit to continue
#! /bin/bash
# Returns YES if installed otherwise return NO
check_curl_installed() {
echo >&2 "Before running, the command will be printed below."
echo >&2 "Press any key to approve running it"
read -n 1 -s -r -p "which curl"
echo ""
if which curl > /dev/null; then
echo "YES"
else
echo "NO"
fi
}
main() {
RESULT=$(check_curl_installed)
echo $RESULT
echo x${RESULT}x
}
main "$#"
exit 0
This is the output
user#computer:tmp$ ./check_curl_installed.sh
Before running, the command it will be printed below.
Press any key to approve running it
which curlYES
x YESx
Instead of using the output of the function, use its exit status.
check_curl_installed() {
echo >&2 "Before running, the command will be printed below."
echo >&2 "Press any key to approve running it"
read -n 1 -s -r -p "which curl"
echo ""
if which curl > /dev/null; then
return 0
else
return 1
fi
}
if check_curl_installed
then
# do something
else
# do something else
fi
how about this modified version to get only the Y key to answer...
#! /bin/bash
# Returns YES if installed otherwise return NO
check_curl_installed() {
echo >&2 "Before running, the command will be printed below."
echo >&2 "Press Y key to approve running it"
read -n 1 -r -s -p "Which curl?"
if [[ $REPLY =~ ^[Yy]$ ]]; then
if which curl > /dev/null ; then
printf "\nYES It is installed\n"
else
printf "\nNO It is not installed\n"
fi
else
printf "\nExiting - not checking\n"
fi
}
main() {
check_curl_installed
}
main "$#"
exit 0
Your echo result is, i think just pulling the first line of the check_curl_installed function...
Maybe if result was set to an array?
my testing around has shown that it's forgetting variables in the function at the higher main function. I even tried exporting to get the main function to work, but to no avail. I'm not super strong in bash, so i apologize on that.
Also might work better to put the echos inside each function, instead of shoving them into a variable...
Most, if not all, languages, only return one value from a function. Maybe this is why your output don't do as you want? a quick search brought this up https://unix.stackexchange.com/questions/408543/how-can-a-bash-function-return-multiple-values

Code in Shell Script runs before other Operations

This same problem is better formulated in a question posted to the Unix & Linux StackExchange community.
I am programming a script which opens on a key press, open a new terminal (gnome-terminal), runs scrot (screenshot tool), saves the picture with random name to a directory, uploads it to pomf.cat and copies the link to the clipboard.
This works fine. What im trying to do now is, when uploading is done, close the terminal.
My script works like this:
Shortcut (PrtScr) -> gnome-terminal -e "python path/to/script.py" -> Start Scrot -> Save File (and remember path to file) -> bash script2.sh path/to/picture -> Upload to pomf.cat -> Get the link -> Put into clipboard via "xclip -selection clipboard"
Since i want to close the Terminal after putting the String into Clipboard, i added this:
eval $(printf $link | xclip -selection clipboard && sleep 1 && pkill terminal)
The problem with this is, nothing gets copied into clipboard and the terminal closes.
However, without "&& sleep 1 && pkill terminal" the link gets copied but the terminal stays open.
Thanks in advance.
//EDIT
First Script (for running scrot)
#!/usr/bin/env python
import os
import uuid
import time
def rstring(string_length=10):
random = str(uuid.uuid4())
random = random.upper()
random = random.replace("-","")
return random[0:string_length]
randomString = rstring(16)
os.system("scrot -s -q 100 /home/timon/screenshots/" + randomString + ".jpg")
while True:
processRead = os.popen("ps aux | grep \"scrot -s\" | cat").read()
if "scrot -s" not in processRead:
time.sleep(1)
else:
break
system.sleep(3)
os.system("/home/timon/.screenshot_stuff/./screen.sh /home/timon/screenshots/" + randomString + ".jpg")
Second Script (for uploading the screenshot)
#!/usr/bin/env bash
dest_url='https://cuntflaps.me/upload.php'
return_url='https://a.cuntflaps.me'
if [[ -n "${1}" ]]; then
file="${1}"
if [ -f "${file}" ]; then
printf "Uploading ${file}..."
my_output=$(curl --silent -sf -F files[]="#${file}" "${dest_url}")
n=0 # Multipe tries
while [[ $n -le 3 ]]; do
printf "try #${n}...\n"
if [[ true ]]; then
return_file=$(echo "$my_output" | grep "url" | sed 's/\,//g' | sed 's/\\//g' | sed 's/\"//g' | sed 's/\url://g' | tr -d ' ')
printf 'done.\n'
break
else
printf 'failed.\n'
((n = n +1))
fi
done
printf "$return_file" | xclip -selection clipboard && pkill terminal
else
printf 'Error! File does not exist!\n'
exit 1
fi
else
printf 'Error! You must supply a filename to upload!\n'
exit 1
fi
So in the end i came up with my own solution.
The problem seemed to be xclip itself.
Now i use "xsel --clipboard --input", which seems to work, even after exiting directly.

Bash <read> printing new lines, how to prevent

So, I have a situation where I want to print output onto the output that was just posted by a user, and I wrote this short thing to make sure it's possible.
echo -e "Cmd> \c" && read cmd && echo "-append_something"
Although it would seem that read prints a new line, on "enter". Is there a way to cancel this? Or any other way I could print this output onto the same line.
Seeing input in real-time IS necessary.
Expected output: Cmd> <whatever>-append_something
Use -s option to read:
echo -e "Cmd> \c" && read -s cmd && echo "-append_something"
Update:
Using classic ANSI screen cursor manipulation should do the trick:
echo -e "Cmd> \c"$'\e[s' && read cmd && echo $'\e[u'"${cmd}-append_something"
Simpler:
echo -ne "Cmd> \e[s" && read cmd && echo $'\e[u'"${cmd}-append_something"
Another shorter:
read -p "Cmd> "$'\e[s' cmd && echo $'\e[u'"${cmd}-append_something"
Something even more manual:
read -p "Cmd> " cmd && echo $'\e[A\e[5C'"${cmd}-append_something"

Resources