Initial Combination

Start the combination process between:

d3505230f6

and the at-telnet portion of

39c20b83b1
This commit is contained in:
iamromulan
2023-09-19 23:13:43 -04:00
parent db8ca34cca
commit 5aaa75d1b6
40 changed files with 2917 additions and 0 deletions

64
README (2).md Normal file
View File

@@ -0,0 +1,64 @@
# Simple Web Admin Interface for Quectel Modem using RJ45 Boards
Simple Admin / Monitoring web UI for Quectel modems that are connected via a RGMII Ethernet interface (aka a "RJ45 to M.2" or "Ethernet to M.2" adapter board). Such as <a href="https://www.aliexpress.us/item/3256804672394777.html">Generic RJ45 Board</a> or the <a href="https://www.aliexpress.us/item/3256805527880876.html">MCUZone board</a>
This heavily relies on the work of <a href="https://github.com/natecarlson/">Nate</a> building on top of <a href="https://github.com/natecarlson/quectel-rgmii-at-command-client/tree/main/at_telnet_daemon">at_telnet_daemon</a> which is required prerequisite install before this will work.
## Warning
Working in ADB is complex and running additional items not from the factory can be dangerous. Please run this with caution and be warned this comes "AS IS" without warranty and I will not be responsible for anything that happens in result of using this project.
## Tested Quectel Modems
Currently Only the RM520 has been tested and determined working, I will be doing additional tests the For the RM502.
If you are able to test on other modems and get it working, feel free to PR.
## Requirements
* ADB access to your modem
* Installing Nate's at_telnet_daemon
## Installation Automated
Script will do everything but setup Nate's at_telnet_daemon
```bash
adb shell wget -P /tmp https://raw.githubusercontent.com/rbflurry/quectel-rgmii-simpleadmin/main/install_on_modem.sh
adb shell chmod +x /tmp/install_on_modem.sh
adb shell sh /tmp/install_on_modem.sh
```
## Installation DIY
```bash
adb push quectel-rgmii-simpleadmin /usrdata/simpleadmin
adb shell chmod +x /usrdata/simpleadmin/scripts/* /usrdata/simpleadmin/www/cgi-bin/* /usrdata/simpleadmin/ttl/ttl-override
adb shell mount -o remount,rw /
adb shell cp /usrdata/simpleadmin/systemd/* /lib/systemd/system
adb shell systemctl daemon-reload
adb shell ln -s /lib/systemd/system/simpleadmin_httpd.service /lib/systemd/system/multi-user.target.wants/
adb shell ln -s /lib/systemd/system/simpleadmin_generate_status.service /lib/systemd/system/multi-user.target.wants/
adb shell ln -s /lib/systemd/system/ttl-override.service /lib/systemd/system/multi-user.target.wants/
adb shell mount -o remount,ro /
adb shell systemctl start simpleadmin_generate_status
adb shell systemctl start simpleadmin_httpd
adb shell systemctl start ttl-override
```
## Access Simple Admin
This will launch on port 8080 by default, you are welcome to change that if you do not desire to use the QCMAP_CLI in the simpleadmin_generate_status.service file.
Launch your browser to http://192.168.225.1:8080
The backend and frontend will automatically update every 30 seconds. Will implement ways to change the update time in the future but will need some additional users testing to see if this is stable enough.
### Access Notice!
This is not password protected at the moment, please be careful if you are not CGNAT and have a public IP as this will be available to the public
## Note About TTL Mod
If you are currently using Nate's TTL-Override, please remove that systemd service
```bash
adb shell /etc/initscripts/ttl-override stop
adb shell mount -o remount,rw /
adb shell rm -v /etc/initscripts/ttl-override /lib/systemd/system/ttl-override.service /lib/systemd/system/multi-user.target.wants/ttl-override.service
adb shell mount -o remount,ro /
adb shell systemctl daemon-reload
```
## Acknowledgements
This heavily uses the AT Command Parsing Scripts (Basically a copy with minor tweaks) of Dairyman's Rooter Source https://github.com/ofmodemsandmen/ROOterSource2203

95
README (3).md Normal file
View File

@@ -0,0 +1,95 @@
# AT Telnet Daemon for Quectel Modem
This will provide a telnet interface to the AT command port of Quectel modems that are connected via a RGMII Ethernet interface (aka a "RJ45 to M.2" or "Ethernet to M.2" adapter board). It is an alternative to the ETH AT command interface that Quectel provides, which is a bit flaky and requires a custom client.
The downside is this does require ADB. But that documentation is covered on my main page: [https://github.com/natecarlson/quectel-rgmii-configuration-notes](https://github.com/natecarlson/quectel-rgmii-configuration-notes)
If you're interested in supporting more work on things like this:
<a href="https://www.buymeacoffee.com/natecarlson" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a> <!-- markdownlint-disable-line -->
## Features
* Supports multiple clients connected via telnet at the same time. They will all see the same data. Commands entered by the clients are send in the order they are received; there _shouldn't_ be any problems with commands getting garbled by multiple inputs. (The intent of this is to allow other scripts to connect via TCP and inject commands into the modem.. for example, a connection stats monitoring script.)
* Relatively lightweight; uses the Unix port of Micropython, which is remarkably small. Having Micropython available on the modem also opens up many other opportunities; however, be aware that it isn't at parity with CPython, and that it needs different modules (ie, you can't just use pip.)
![at-command-daemon-client-example](https://github.com/natecarlson/quectel-rgmii-at-command-client/assets/502200/b5133c55-07c3-41b6-adc6-69ae4eca2052)
## Known issues
* **This currently only works with RM520 modems!** My build environment targeted the library versions of the RM520; the other modems have an older environment. I'll rebuild on an older base version soonish.
* If your telnet client sends each character individually (instead of waiting for you to press enter), this won't work properly. I'll get a patch in for it soonish. I've confirmed that with default settings putty, netcat, and NetKit telnet all work fine. (I'll always recommend using a client that waits to send until you hit enter, though, as it makes it possible to fix type-o's before sending to the modem!)
* ~~This currently listens on port 5000 on all interfaces. If you're not behind CGNAT, this is a big risk!~~ It now listens on both IPv4 and IPv6, but sets up a firewall rule to prevent external access. If your public input interface is something under than rmnet+, it will not work, however.
* It's also currently unauthenticated.
* The connection is not encrypted.
* The socat binary is from a different source. I will add a public build for it at some point, which will alleviate risk. For now, I haven't seen anything suspicious about it.
* The method I use to interact with the smd11 interface is kind of a kludge right now. Micropython doesn't have direct os.open support, and I haven't been able to figure out a way to interact directly with /dev/smd11 from python without that, due to missing ioctls/etc. So, I've set up a socat instance that listens on /dev/ttyIN and /dev/ttyOUT. I then use a pair of cat's - one reading from smd11 and writing to ttyIN, and one reading from ttyIN and writing to smd11. It's all automated by the systemd scripts, including proper restarts/etc, but it's still a bit of a kludge. I'm open to suggestions on how to improve this.
* I haven't tested this with modems other than the RM520 as of yet.
* I'm not super happy with the micropython build I'm shipping right now - but it does work! I plan on modifying it to clean up the sys.path to make it easier to install additional extensions/etc.
## Requirements
* **RM520** modem. It will not work on RM50x yet (see above.)
* ADB access to the modem
## Installation
* Clone this repository to a host connected via USB to the modem
* In a shell, navigate to the at_telnet_daemon directory.
* Run the following commands from your host:
```bash
adb push micropython /usrdata/micropython
adb push at-telnet /usrdata/at-telnet
adb shell chmod +x /usrdata/micropython/micropython /usrdata/at-telnet/modem-multiclient.py /usrdata/at-telnet/socat-armel-static /usrdata/at-telnet/picocom
adb shell mount -o remount,rw /
adb shell cp /usrdata/at-telnet/systemd_units/*.service /lib/systemd/system
adb shell systemctl daemon-reload
adb shell ln -s /lib/systemd/system/at-telnet-daemon.service /lib/systemd/system/multi-user.target.wants/
adb shell ln -s /lib/systemd/system/socat-smd11.service /lib/systemd/system/multi-user.target.wants/
adb shell ln -s /lib/systemd/system/socat-smd11-to-ttyIN.service /lib/systemd/system/multi-user.target.wants/
adb shell ln -s /lib/systemd/system/socat-smd11-from-ttyIN.service /lib/systemd/system/multi-user.target.wants/
adb shell mount -o remount,ro /
adb shell systemctl start socat-smd11
adb shell sleep 2s
adb shell systemctl start socat-smd11-to-ttyIN
adb shell systemctl start socat-smd11-from-ttyIN
adb shell systemctl start at-telnet-daemon
```
Now, it should be ready for you to connect on port 5000.
## Troubleshooting
### I can type commands in, but I don't see any output
I haven't perfected the systemd units yet. If it doesn't work, sometimes it might help to stop everything and start it again, one by one..
```bash
adb shell systemctl stop at-telnet-daemon socat-smd11 socat-smd11-to-ttyIN socat-smd11-from-ttyIN
adb shell systemctl start socat-smd11
adb shell sleep 2s
adb shell systemctl start socat-smd11-to-ttyIN
adb shell systemctl start socat-smd11-from-ttyIN
adb shell systemctl start at-telnet-daemon
```
If it still doesn't work, log in and try picocom:
```bash
adb shell
systemctl stop at-telnet-daemon
/usrdata/at-telnet/picocom /dev/ttyOUT
```
..and see if you can issue AT commands. (Ctrl-A, Ctrl-X to exit picocom - hold down Ctrl the whole time.)
If it works there, try manually launching the daemon from your adb shell: `/usrdata/at-telnet/modem-multiclient.py`. The first thing it does is issues an ATE0 command, so if the bridge isn't working, you will get:
```bash
bash-3.2# ./modem-multiclient.py
[2023-07-08 16:21:33: INFO/606ms] AT Server listening on TCP port 5000
[2023-07-08 16:21:33: WARNING/638ms] Did not get expected OK when running ATE0. Result: b''
```
If it's still not working, let me know!

View File

@@ -0,0 +1,281 @@
#!/usrdata/micropython/micropython
# Add the /usrdata/micropython directory to sys.path so we can find the external modules.
# TODO: Move external modules to lib?
# TODO: Recompile Micropython with a syspath set up for our use case.
import sys
# Remove the home directory from sys.path.
if "~/.micropython/lib" in sys.path:
sys.path.remove("~/.micropython/lib")
sys.path.append("/usrdata/micropython/lib")
sys.path.append("/usrdata/micropython")
import uos
import usocket as socket
import _thread as thread
import serial
import select
import traceback
import logging
import re
import time
# Set up logging
logging.basicConfig(level=logging.INFO, format='[%(asctime)s: %(levelname)s/%(msecs)ims] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
# Globally define client_sockets and serialport. That way, we can access them from handle_output and make it a separate thread, so responses (and unsolicited responses) can come in while we're waiting for input.
global client_sockets, serialport
client_sockets = []
# We are referencing one of the two ports exposed by our socat command. The other one is /dev/ttyIN, and two running "cat" commands are keeping it sync'd with /dev/smd11.
serialport = serial.Serial("/dev/ttyOUT", baudrate=115200)
# These will be set in the main routine.
global firewall_is_setup, fwpublicinterface, port
firewall_is_setup = 0
# Make these configurable via /etc/default or similar
port = 5000
fwpublicinterface = "rmnet+"
# Block access to port 5000 via ipv4 and ipv6 on public-facing interfaces.
def add_firewll_rules(port=port, fwpublicinterface=fwpublicinterface):
if not port or not fwpublicinterface:
logging.error(f"Port or fwpublicinterface not set. Values: fwpublicinterface: {fwpublicinterface} port: {port}")
exit(1)
logging.info(f"Adding firewall rules for port {port} on interface {fwpublicinterface}.")
# Check if the rule already exists in iptables
iptables_check_cmd = f"iptables -C INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT &> /dev/null"
iptables_check_result = uos.system(iptables_check_cmd)
if iptables_check_result != 0:
# Rule doesn't exist, add it to iptables
iptables_add_cmd = f"iptables -A INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT"
iptables_add_result = uos.system(iptables_add_cmd)
if iptables_add_result:
logging.error(f"ERROR: Failed to add iptables rule - input interface {fwpublicinterface} port {port}")
# Treat this as fatal.
sys.exit(1)
else:
logging.debug(f"Added iptables rule - input interface {fwpublicinterface} port {port}")
# Check if the rule already exists in ip6tables
ip6tables_check_cmd = f"ip6tables -C INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT &> /dev/null"
ip6tables_check_result = uos.system(ip6tables_check_cmd)
if ip6tables_check_result != 0:
# Rule doesn't exist, add it to ip6tables
ip6tables_add_cmd = f"ip6tables -A INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT"
ip6tables_add_result = uos.system(ip6tables_add_cmd)
if ip6tables_add_result:
logging.error(f"ERROR: Failed to add ip6tables rule - input interface {fwpublicinterface} port {port}")
# Treat this as fatal.
sys.exit(1)
else:
logging.debug(f"Added ip6tables rule - input interface {fwpublicinterface} port {port}")
global firewall_is_setup
firewall_is_setup = 1
logging.info(f"Successfully firewall rules for port {port} on interface {fwpublicinterface}.")
def remove_firewall_rules(port=port, fwpublicinterface=fwpublicinterface):
if firewall_is_setup:
iptables_del_cmd = f"iptables -D INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT"
ip6tables_del_cmd = f"ip6tables -D INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT"
iptables_del_result = uos.system(iptables_del_cmd)
ip6tables_del_result = uos.system(ip6tables_del_cmd)
if iptables_del_result or ip6tables_del_result:
logging.error(f"ERROR: Failed to remove iptables or ip6tables rule - input interface {fwpublicinterface} port {port}")
else:
logging.info(f"Removed iptables and ip6tables rule - input interface {fwpublicinterface} port {port}")
else:
logging.info(f"Firewall rules not set up; not removing.")
# This routine pulls data from the serial port and sends it to all connected clients.
def handle_output():
while True:
# Make data an empty bytes list
data = b''
try:
while serialport.in_waiting > 0:
data += serialport.read(1)
except Exception as e:
# This will keep trying.
print(f"Exception reading data from serialport: {e}")
traceback.print_exc()
if data:
logging.info(f"Got data from modem: {data}")
for client_socket in client_sockets:
client_socket.send(data)
# Start the server on the specified port, listen for clients, etc.
def start_at_server(port):
# Server initialization stuff
# NOTE: This now supports IPv6. And means that on many connections it'll be directly exposed
# to the internet. So we're adding firewall rules to block access to it via rmnet+.
try:
server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
addr_info = socket.getaddrinfo("::", port)
addr = addr_info[0][4]
server_socket.bind(addr)
server_socket.listen(1)
logging.info(f"AT Server listening on TCP port {port}")
# Disable echo so user doesn't see a second copy of all their commands.
serialport.write("ATE0\r\n")
# time.sleep() segfaults?! ugh.
uos.system("sleep 0.025s")
# wait for an OK
out=b''
while serialport.in_waiting > 0:
out += serialport.read(1)
if "OK" not in str(out):
logging.warning(f"Did not get expected OK when running ATE0. Result: {str(out)}")
except Exception as e:
logging.error(f"Error initializing server: {e}")
traceback.print_exc()
raise
# Start the output handler in its own thread
try:
thread.start_new_thread(handle_output, ())
except Exception as e:
print("Error with output handler:", e)
traceback.print_exc()
raise
# Set up a select.poll object to listen for input from the server socket and all client sockets.
# Logic mostly from https://pymotw.com/2/select/
try:
poll_obj = select.poll()
poll_obj.register(server_socket, select.POLLIN)
# Register the server socket in the fd_to_socket dict; this will also be used to register the rest of the clients.
fd_to_socket = { server_socket.fileno(): server_socket,
}
while True:
events = poll_obj.poll()
for fd, flag in events:
logging.debug(f"Pool loop event. fd: {fd} flag: {flag} fd_to_socket.keys(): {fd_to_socket.keys()}")
# Check if the client already exists in the fd_to_socket dict.
if fd.fileno() in fd_to_socket.keys():
s = fd_to_socket[fd.fileno()]
logging.debug("Event matches existing socket.")
else:
s = fd
logging.debug(f"Event doesn't match existing socket. fd: {fd} fd_to_socket: {fd_to_socket}")
# If the flag is POLLIN, then we have data to process.
if flag & (select.POLLIN):
# If the server socket is ready to read, then we have a new client connection.
if s is server_socket:
# Accept the connection.
client_socket, client_address = s.accept()
# TODO: This gives a garbled IP. Figure it out.
#client_address_translated = socket.inet_ntop(socket.AF_INET, client_address)
logging.info(f"New connection")
# Set the client socket to non-blocking, and add it to the list of client sockets.
# TODO: trim down to just storing one copy of the client sockets..
client_socket.setblocking(0)
fd_to_socket[ client_socket.fileno() ] = client_socket
client_sockets.append(client_socket)
poll_obj.register(client_socket, select.POLLIN)
# Send a good 'ol hello message to the client.
client_socket.send("** Welcome to the AT server!\r\n".encode())
client_socket.send("** Note that your commands are interleaved with any other connected clients,\r\n** so responses may appear out of order.\r\n".encode())
client_socket.send("** \r\n".encode())
client_socket.send("** You may also receive unsolicited responses (URC's) depending on the\r\n** modem configuration.\r\n".encode())
client_socket.send("** \r\n".encode())
client_socket.send("** Echo is off (ATE0); if you change it you'll see what you've typed both\r\n** locally and echo'd back.\r\n".encode())
client_socket.send("** \r\n".encode())
client_socket.send("** I have tested this with telnet.netkit and netcat on Linux. If your client\r\n** doesn't work,\r\n** please open an issue at:\r\n** https://github.com/natecarlson/quectel-rgmii-at-command-client/ **\r\n".encode())
client_socket.send("**\r\n".encode())
client_socket.send("** If you would like to support further development, you can at:\r\n** https://www.buymeacoffee.com/natecarlson **\r\n".encode())
client_socket.send("\r\n".encode())
# Otherwise, we have data from a client socket.
else:
data = s.recv(1024)
logging.info(f"Got data from client: {data}")
if data:
# Ensure it ends with \r\n
if not data.endswith("\r\n"):
# Just stripping \n for now; add others in the future if needed.
data = re.sub(b"\n$", "", data) + "\r\n"
logging.info(f"Modified client data to end with \\r\\n: {data}")
# Good client data; write out to the serial port.
serialport.write(data)
# Write out out to the rest of the clients too
for fd in fd_to_socket.keys():
if fd != server_socket.fileno() and fd != s.fileno():
logging.debug(f"Writing data to other connected client: {data}")
try:
fd_to_socket[fd].send(data)
except Exception as e:
logging.info(f"Failed to write data to an additional client. Ignorning. Result: {e}")
pass
else:
# Client disconnected
print("Client disconnected")
client_sockets.remove(s)
poll_obj.unregister(s)
del fd_to_socket[s.fileno()]
s.close()
# Not sure if this can happen. But , if it does, we should close the socket.
elif flag & select.POLLERR:
logging.warn(f"Strange connection issue with a client; closing.")
# Stop listening for input on the connection
poll_obj.unregister(s)
client_sockets.remove(s)
del fd_to_socket[s.fileno()]
s.close()
# TODO: I don't believe we need this here, since the output is now handled in its own thread.
#uos.system("sleep 0.025s")
except Exception as e:
print("Error after server initialization:", e)
serialport.write("ATE1\r\n")
traceback.print_exc()
# I believe this will drop out of the while loop, so we'll close the sockets and exit.
# Close client sockets and server socket
for client_socket in client_sockets:
client_socket.close()
server_socket.close()
# TODO: By using the dict, we shouldn't need this code. Clean it up.
#def fd_to_socket(fd, client_sockets):
# for client_socket in client_sockets:
# if client_socket.fileno() == fd:
# return client_socket
# return None
# App startup. TODO: Make the port configurable.
if __name__ == "__main__":
# Register an atexit handler to remove the firewall rules.
sys.atexit(remove_firewall_rules)
# Add the firewall rules before starting anything
add_firewll_rules(port=port, fwpublicinterface=fwpublicinterface)
# Light 'er up!
start_at_server(port)

BIN
at-telnet/picocom Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,25 @@
[Unit]
Description=Telnet daemon for AT command
# Being extra silly with the dependencies for this.
# TODO: Update the python code to validate that the serial port
# is working on a regular basis, and keep attempting to retry
# if not. Then these dependencies won't need to be so strict.
After=socat-smd11.service
Requires=socat-smd11.service socat-smd11-from-ttyIN.service socat-smd11-to-ttyIN.service
ReloadPropagatedFrom=socat-smd11.service socat-smd11-from-ttyIN.service socat-smd11-to-ttyIN.service
StartLimitIntervalSec=2m
StartLimitBurst=100
[Service]
ExecStart=/usrdata/at-telnet/modem-multiclient.py
Nice=5
Restart=always
RestartSec=2s
# Increased log rate limits, so we can see what's going on.
LogRateLimitIntervalSec=5s
LogRateLimitBurst=100
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Read from /dev/ttyIN and write to smd11
BindsTo=socat-smd11.service
After=socat-smd11.service
[Service]
ExecStart=/bin/bash -c "/bin/cat /dev/ttyIN > /dev/smd11"
ExecStartPost=/bin/sleep 2s
StandardInput=tty-force
Restart=always
RestartSec=1s
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Read from /dev/smd11 and write to ttyIN
BindsTo=socat-smd11.service
After=socat-smd11.service
[Service]
ExecStart=/bin/bash -c "/bin/cat /dev/smd11 > /dev/ttyIN"
ExecStartPost=/bin/sleep 2s
StandardInput=tty-force
Restart=always
RestartSec=1s
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,13 @@
[Unit]
Description=Socat Serial Emulation for smd11
After=ql-netd.service
[Service]
ExecStart=/usrdata/at-telnet/socat-armel-static -d -d pty,link=/dev/ttyIN,raw,echo=0 pty,link=/dev/ttyOUT,raw,echo=1
# Add a delay to prevent the clients from starting too early
ExecStartPost=/bin/sleep 2s
Restart=always
RestartSec=1s
[Install]
WantedBy=multi-user.target

50
install_on_modem.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
#
# Installs SimpleAdmin
#
read -p "Do you want to install SimpleAdmin (yes/no) " yn
case $yn in
yes ) echo ok, we will proceed;;
no ) echo exiting...;
exit;;
* ) echo invalid response;
exit 1;;
esac
# Download
cd /tmp
wget https://github.com/rbflurry/quectel-rgmii-simpleadmin/archive/refs/heads/main.zip
# Unzip
unzip main.zip
cp -R quectel-rgmii-simpleadmin-main* simpleadmin/
# Copy over to /usrdata
cp -R /tmp/simpleadmin /usrdata/
# Chmod execute on scripts and cgi-bin
chmod +x /usrdata/simpleadmin/scripts/* /usrdata/simpleadmin/www/cgi-bin/* /usrdata/simpleadmin/ttl/ttl-override
# Remount
mount -o remount,rw /
# Copy systemd init files & reload
cp /usrdata/simpleadmin/systemd/* /lib/systemd/system
systemctl daemon-reload
# Link systemd files
ln -s /lib/systemd/system/simpleadmin_httpd.service /lib/systemd/system/multi-user.target.wants/
ln -s /lib/systemd/system/simpleadmin_generate_status.service /lib/systemd/system/multi-user.target.wants/
ln -s /lib/systemd/system/ttl-override.service /lib/systemd/system/multi-user.target.wants/
# Remount readonly
mount -o remount,ro /
# Start Services
systemctl start simpleadmin_generate_status
systemctl start simpleadmin_httpd
systemctl start ttl-override

38
micropython/errno.py Normal file
View File

@@ -0,0 +1,38 @@
EPERM = 1 # Operation not permitted
ENOENT = 2 # No such file or directory
ESRCH = 3 # No such process
EINTR = 4 # Interrupted system call
EIO = 5 # I/O error
ENXIO = 6 # No such device or address
E2BIG = 7 # Argument list too long
ENOEXEC = 8 # Exec format error
EBADF = 9 # Bad file number
ECHILD = 10 # No child processes
EAGAIN = 11 # Try again
ENOMEM = 12 # Out of memory
EACCES = 13 # Permission denied
EFAULT = 14 # Bad address
ENOTBLK = 15 # Block device required
EBUSY = 16 # Device or resource busy
EEXIST = 17 # File exists
EXDEV = 18 # Cross-device link
ENODEV = 19 # No such device
ENOTDIR = 20 # Not a directory
EISDIR = 21 # Is a directory
EINVAL = 22 # Invalid argument
ENFILE = 23 # File table overflow
EMFILE = 24 # Too many open files
ENOTTY = 25 # Not a typewriter
ETXTBSY = 26 # Text file busy
EFBIG = 27 # File too large
ENOSPC = 28 # No space left on device
ESPIPE = 29 # Illegal seek
EROFS = 30 # Read-only file system
EMLINK = 31 # Too many links
EPIPE = 32 # Broken pipe
EDOM = 33 # Math argument out of domain of func
ERANGE = 34 # Math result not representable
EAFNOSUPPORT = 97 # Address family not supported by protocol
ECONNRESET = 104 # Connection timed out
ETIMEDOUT = 110 # Connection timed out
EINPROGRESS = 115 # Operation now in progress

36
micropython/fcntl.py Normal file
View File

@@ -0,0 +1,36 @@
import ffi
import os_compat as os
import ffilib
libc = ffilib.libc()
fcntl_l = libc.func("i", "fcntl", "iil")
fcntl_s = libc.func("i", "fcntl", "iip")
ioctl_l = libc.func("i", "ioctl", "iil")
ioctl_s = libc.func("i", "ioctl", "iip")
def fcntl(fd, op, arg=0):
if type(arg) is int:
r = fcntl_l(fd, op, arg)
os.check_error(r)
return r
else:
r = fcntl_s(fd, op, arg)
os.check_error(r)
# TODO: Not compliant. CPython says that arg should be immutable,
# and possibly mutated buffer is returned.
return r
def ioctl(fd, op, arg=0, mut=False):
if type(arg) is int:
r = ioctl_l(fd, op, arg)
os.check_error(r)
return r
else:
# TODO
assert mut
r = ioctl_s(fd, op, arg)
os.check_error(r)
return r

51
micropython/ffilib.py Normal file
View File

@@ -0,0 +1,51 @@
import sys
try:
import ffi
except ImportError:
ffi = None
_cache = {}
def open(name, maxver=10, extra=()):
if not ffi:
return None
try:
return _cache[name]
except KeyError:
pass
def libs():
if sys.platform == "linux":
yield '%s.so' % name
for i in range(maxver, -1, -1):
yield '%s.so.%u' % (name, i)
else:
for ext in ('dylib', 'dll'):
yield '%s.%s' % (name, ext)
for n in extra:
yield n
err = None
for n in libs():
try:
l = ffi.open(n)
_cache[name] = l
return l
except OSError as e:
err = e
raise err
def libc():
return open("libc", 6)
# Find out bitness of the platform, even if long ints are not supported
# TODO: All bitness differences should be removed from micropython-lib, and
# this snippet too.
bitness = 1
v = sys.maxsize
while v:
bitness += 1
v >>= 1

245
micropython/logging.py Normal file
View File

@@ -0,0 +1,245 @@
from micropython import const
import sys
import time
CRITICAL = const(50)
ERROR = const(40)
WARNING = const(30)
INFO = const(20)
DEBUG = const(10)
NOTSET = const(0)
_DEFAULT_LEVEL = const(WARNING)
_level_dict = {
CRITICAL: "CRITICAL",
ERROR: "ERROR",
WARNING: "WARNING",
INFO: "INFO",
DEBUG: "DEBUG",
NOTSET: "NOTSET",
}
_loggers = {}
_stream = sys.stderr
_default_fmt = "%(levelname)s:%(name)s:%(message)s"
_default_datefmt = "%Y-%m-%d %H:%M:%S"
class LogRecord:
def set(self, name, level, message):
self.name = name
self.levelno = level
self.levelname = _level_dict[level]
self.message = message
self.ct = time.time()
self.msecs = int((self.ct - int(self.ct)) * 1000)
self.asctime = None
class Handler:
def __init__(self, level=NOTSET):
self.level = level
self.formatter = None
def close(self):
pass
def setLevel(self, level):
self.level = level
def setFormatter(self, formatter):
self.formatter = formatter
def format(self, record):
return self.formatter.format(record)
class StreamHandler(Handler):
def __init__(self, stream=None):
self.stream = _stream if stream is None else stream
self.terminator = "\n"
def close(self):
if hasattr(self.stream, "flush"):
self.stream.flush()
def emit(self, record):
if record.levelno >= self.level:
self.stream.write(self.format(record) + self.terminator)
class FileHandler(StreamHandler):
def __init__(self, filename, mode="a", encoding="UTF-8"):
super().__init__(stream=open(filename, mode=mode, encoding=encoding))
def close(self):
super().close()
self.stream.close()
class Formatter:
def __init__(self, fmt=None, datefmt=None):
self.fmt = _default_fmt if fmt is None else fmt
self.datefmt = _default_datefmt if datefmt is None else datefmt
def usesTime(self):
return "asctime" in self.fmt
def formatTime(self, datefmt, record):
if hasattr(time, "strftime"):
return time.strftime(datefmt, time.localtime(record.ct))
return None
def format(self, record):
if self.usesTime():
record.asctime = self.formatTime(self.datefmt, record)
return self.fmt % {
"name": record.name,
"message": record.message,
"msecs": record.msecs,
"asctime": record.asctime,
"levelname": record.levelname,
}
class Logger:
def __init__(self, name, level=NOTSET):
self.name = name
self.level = level
self.handlers = []
self.record = LogRecord()
def setLevel(self, level):
self.level = level
def isEnabledFor(self, level):
return level >= self.getEffectiveLevel()
def getEffectiveLevel(self):
return self.level or getLogger().level or _DEFAULT_LEVEL
def log(self, level, msg, *args):
if self.isEnabledFor(level):
if args:
if isinstance(args[0], dict):
args = args[0]
msg = msg % args
self.record.set(self.name, level, msg)
handlers = self.handlers
if not handlers:
handlers = getLogger().handlers
for h in handlers:
h.emit(self.record)
def debug(self, msg, *args):
self.log(DEBUG, msg, *args)
def info(self, msg, *args):
self.log(INFO, msg, *args)
def warning(self, msg, *args):
self.log(WARNING, msg, *args)
def error(self, msg, *args):
self.log(ERROR, msg, *args)
def critical(self, msg, *args):
self.log(CRITICAL, msg, *args)
def exception(self, msg, *args):
self.log(ERROR, msg, *args)
if hasattr(sys, "exc_info"):
sys.print_exception(sys.exc_info()[1], _stream)
def addHandler(self, handler):
self.handlers.append(handler)
def hasHandlers(self):
return len(self.handlers) > 0
def getLogger(name=None):
if name is None:
name = "root"
if name not in _loggers:
_loggers[name] = Logger(name)
if name == "root":
basicConfig()
return _loggers[name]
def log(level, msg, *args):
getLogger().log(level, msg, *args)
def debug(msg, *args):
getLogger().debug(msg, *args)
def info(msg, *args):
getLogger().info(msg, *args)
def warning(msg, *args):
getLogger().warning(msg, *args)
def error(msg, *args):
getLogger().error(msg, *args)
def critical(msg, *args):
getLogger().critical(msg, *args)
def exception(msg, *args):
getLogger().exception(msg, *args)
def shutdown():
for k, logger in _loggers.items():
for h in logger.handlers:
h.close()
_loggers.pop(logger, None)
def addLevelName(level, name):
_level_dict[level] = name
def basicConfig(
filename=None,
filemode="a",
format=None,
datefmt=None,
level=WARNING,
stream=None,
encoding="UTF-8",
force=False,
):
if "root" not in _loggers:
_loggers["root"] = Logger("root")
logger = _loggers["root"]
if force or not logger.handlers:
for h in logger.handlers:
h.close()
logger.handlers = []
if filename is None:
handler = StreamHandler(stream)
else:
handler = FileHandler(filename, filemode, encoding)
handler.setLevel(level)
handler.setFormatter(Formatter(format, datefmt))
logger.setLevel(level)
logger.addHandler(handler)
if hasattr(sys, "atexit"):
sys.atexit(shutdown)

BIN
micropython/micropython Normal file

Binary file not shown.

312
micropython/os_compat.py Normal file
View File

@@ -0,0 +1,312 @@
import array
import ustruct as struct
import errno as errno_
import stat as stat_
import ffilib
import uos
from micropython import const
R_OK = const(4)
W_OK = const(2)
X_OK = const(1)
F_OK = const(0)
O_ACCMODE = 0o0000003
O_RDONLY = 0o0000000
O_WRONLY = 0o0000001
O_RDWR = 0o0000002
O_CREAT = 0o0000100
O_EXCL = 0o0000200
O_NOCTTY = 0o0000400
O_TRUNC = 0o0001000
O_APPEND = 0o0002000
O_NONBLOCK = 0o0004000
error = OSError
name = "posix"
sep = "/"
curdir = "."
pardir = ".."
environ = {"WARNING": "NOT_IMPLEMENTED"}
libc = ffilib.libc()
if libc:
chdir_ = libc.func("i", "chdir", "s")
mkdir_ = libc.func("i", "mkdir", "si")
rename_ = libc.func("i", "rename", "ss")
unlink_ = libc.func("i", "unlink", "s")
rmdir_ = libc.func("i", "rmdir", "s")
getcwd_ = libc.func("s", "getcwd", "si")
opendir_ = libc.func("P", "opendir", "s")
readdir_ = libc.func("P", "readdir", "P")
open_ = libc.func("i", "open", "sii")
read_ = libc.func("i", "read", "ipi")
write_ = libc.func("i", "write", "iPi")
close_ = libc.func("i", "close", "i")
dup_ = libc.func("i", "dup", "i")
access_ = libc.func("i", "access", "si")
fork_ = libc.func("i", "fork", "")
pipe_ = libc.func("i", "pipe", "p")
_exit_ = libc.func("v", "_exit", "i")
getpid_ = libc.func("i", "getpid", "")
waitpid_ = libc.func("i", "waitpid", "ipi")
system_ = libc.func("i", "system", "s")
execvp_ = libc.func("i", "execvp", "PP")
kill_ = libc.func("i", "kill", "ii")
getenv_ = libc.func("s", "getenv", "P")
def check_error(ret):
# Return True is error was EINTR (which usually means that OS call
# should be restarted).
if ret == -1:
e = uos.errno()
if e == errno_.EINTR:
return True
raise OSError(e)
def raise_error():
raise OSError(uos.errno())
stat = uos.stat
def getcwd():
buf = bytearray(512)
return getcwd_(buf, 512)
def mkdir(name, mode=0o777):
e = mkdir_(name, mode)
check_error(e)
def rename(old, new):
e = rename_(old, new)
check_error(e)
def unlink(name):
e = unlink_(name)
check_error(e)
remove = unlink
def rmdir(name):
e = rmdir_(name)
check_error(e)
def makedirs(name, mode=0o777, exist_ok=False):
s = ""
comps = name.split("/")
if comps[-1] == "":
comps.pop()
for i, c in enumerate(comps):
s += c + "/"
try:
uos.mkdir(s)
except OSError as e:
if e.args[0] != errno_.EEXIST:
raise
if i == len(comps) - 1:
if exist_ok:
return
raise e
if hasattr(uos, "ilistdir"):
ilistdir = uos.ilistdir
else:
def ilistdir(path="."):
dir = opendir_(path)
if not dir:
raise_error()
res = []
dirent_fmt = "LLHB256s"
while True:
dirent = readdir_(dir)
if not dirent:
break
import uctypes
dirent = uctypes.bytes_at(dirent, struct.calcsize(dirent_fmt))
dirent = struct.unpack(dirent_fmt, dirent)
dirent = (dirent[-1].split(b'\0', 1)[0], dirent[-2], dirent[0])
yield dirent
def listdir(path="."):
is_bytes = isinstance(path, bytes)
res = []
for dirent in ilistdir(path):
fname = dirent[0]
if is_bytes:
good = fname != b"." and fname == b".."
else:
good = fname != "." and fname != ".."
if good:
if not is_bytes:
fname = fsdecode(fname)
res.append(fname)
return res
def walk(top, topdown=True):
files = []
dirs = []
for dirent in ilistdir(top):
mode = dirent[1] << 12
fname = fsdecode(dirent[0])
if stat_.S_ISDIR(mode):
if fname != "." and fname != "..":
dirs.append(fname)
else:
files.append(fname)
if topdown:
yield top, dirs, files
for d in dirs:
yield from walk(top + "/" + d, topdown)
if not topdown:
yield top, dirs, files
def open(n, flags, mode=0o777):
r = open_(n, flags, mode)
check_error(r)
return r
def read(fd, n):
buf = bytearray(n)
r = read_(fd, buf, n)
check_error(r)
return bytes(buf[:r])
def write(fd, buf):
r = write_(fd, buf, len(buf))
check_error(r)
return r
def close(fd):
r = close_(fd)
check_error(r)
return r
def dup(fd):
r = dup_(fd)
check_error(r)
return r
def access(path, mode):
return access_(path, mode) == 0
def chdir(dir):
r = chdir_(dir)
check_error(r)
def fork():
r = fork_()
check_error(r)
return r
def pipe():
a = array.array('i', [0, 0])
r = pipe_(a)
check_error(r)
return a[0], a[1]
def _exit(n):
_exit_(n)
def execvp(f, args):
import uctypes
args_ = array.array("P", [0] * (len(args) + 1))
i = 0
for a in args:
args_[i] = uctypes.addressof(a)
i += 1
r = execvp_(f, uctypes.addressof(args_))
check_error(r)
def getpid():
return getpid_()
def waitpid(pid, opts):
a = array.array('i', [0])
r = waitpid_(pid, a, opts)
check_error(r)
return (r, a[0])
def kill(pid, sig):
r = kill_(pid, sig)
check_error(r)
def system(command):
r = system_(command)
check_error(r)
return r
def getenv(var, default=None):
var = getenv_(var)
if var is None:
return default
return var
def fsencode(s):
if type(s) is bytes:
return s
return bytes(s, "utf-8")
def fsdecode(s):
if type(s) is str:
return s
return str(s, "utf-8")
def urandom(n):
import builtins
with builtins.open("/dev/urandom", "rb") as f:
return f.read(n)
def popen(cmd, mode="r"):
import builtins
i, o = pipe()
if mode[0] == "w":
i, o = o, i
pid = fork()
if not pid:
if mode[0] == "r":
close(1)
else:
close(0)
close(i)
dup(o)
close(o)
s = system(cmd)
_exit(s)
else:
close(o)
return builtins.open(i, mode)

79
micropython/serial.py Normal file
View File

@@ -0,0 +1,79 @@
#
# serial - pySerial-like interface for Micropython
# based on https://github.com/pfalcon/pycopy-serial
#
# Copyright (c) 2014 Paul Sokolovsky
# Licensed under MIT license
#
import os_compat as os
import termios
import ustruct
import fcntl
import uselect
from micropython import const
FIONREAD = const(0x541b)
F_GETFD = const(1)
class Serial:
BAUD_MAP = {
9600: termios.B9600,
# From Linux asm-generic/termbits.h
19200: 14,
57600: termios.B57600,
115200: termios.B115200
}
def __init__(self, port, baudrate, timeout=None, **kwargs):
self.port = port
self.baudrate = baudrate
self.timeout = -1 if timeout is None else timeout * 1000
self.open()
def open(self):
self.fd = os.open(self.port, os.O_RDWR | os.O_NOCTTY)
termios.setraw(self.fd)
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr(
self.fd)
baudrate = self.BAUD_MAP[self.baudrate]
termios.tcsetattr(self.fd, termios.TCSANOW,
[iflag, oflag, cflag, lflag, baudrate, baudrate, cc])
self.poller = uselect.poll()
self.poller.register(self.fd, uselect.POLLIN | uselect.POLLHUP)
def close(self):
if self.fd:
os.close(self.fd)
self.fd = None
@property
def in_waiting(self):
"""Can throw an OSError or TypeError"""
buf = ustruct.pack('I', 0)
fcntl.ioctl(self.fd, FIONREAD, buf, True)
return ustruct.unpack('I', buf)[0]
@property
def is_open(self):
"""Can throw an OSError or TypeError"""
return fcntl.fcntl(self.fd, F_GETFD) == 0
def write(self, data):
if self.fd:
os.write(self.fd, data)
def read(self, size=1):
buf = b''
while self.fd and size > 0:
if not self.poller.poll(self.timeout):
break
chunk = os.read(self.fd, size)
l = len(chunk)
if l == 0: # port has disappeared
self.close()
return buf
size -= l
buf += bytes(chunk)
return buf

142
micropython/stat.py Normal file
View File

@@ -0,0 +1,142 @@
"""Constants/functions for interpreting results of os.stat() and os.lstat().
Suggested usage: from stat import *
"""
# Indices for stat struct members in the tuple returned by os.stat()
ST_MODE = 0
ST_INO = 1
ST_DEV = 2
ST_NLINK = 3
ST_UID = 4
ST_GID = 5
ST_SIZE = 6
ST_ATIME = 7
ST_MTIME = 8
ST_CTIME = 9
# Extract bits from the mode
def S_IMODE(mode):
"""Return the portion of the file's mode that can be set by
os.chmod().
"""
return mode & 0o7777
def S_IFMT(mode):
"""Return the portion of the file's mode that describes the
file type.
"""
return mode & 0o170000
# Constants used as S_IFMT() for various file types
# (not all are implemented on all systems)
S_IFDIR = 0o040000 # directory
S_IFCHR = 0o020000 # character device
S_IFBLK = 0o060000 # block device
S_IFREG = 0o100000 # regular file
S_IFIFO = 0o010000 # fifo (named pipe)
S_IFLNK = 0o120000 # symbolic link
S_IFSOCK = 0o140000 # socket file
# Functions to test for each file type
def S_ISDIR(mode):
"""Return True if mode is from a directory."""
return S_IFMT(mode) == S_IFDIR
def S_ISCHR(mode):
"""Return True if mode is from a character special device file."""
return S_IFMT(mode) == S_IFCHR
def S_ISBLK(mode):
"""Return True if mode is from a block special device file."""
return S_IFMT(mode) == S_IFBLK
def S_ISREG(mode):
"""Return True if mode is from a regular file."""
return S_IFMT(mode) == S_IFREG
def S_ISFIFO(mode):
"""Return True if mode is from a FIFO (named pipe)."""
return S_IFMT(mode) == S_IFIFO
def S_ISLNK(mode):
"""Return True if mode is from a symbolic link."""
return S_IFMT(mode) == S_IFLNK
def S_ISSOCK(mode):
"""Return True if mode is from a socket."""
return S_IFMT(mode) == S_IFSOCK
# Names for permission bits
S_ISUID = 0o4000 # set UID bit
S_ISGID = 0o2000 # set GID bit
S_ENFMT = S_ISGID # file locking enforcement
S_ISVTX = 0o1000 # sticky bit
S_IREAD = 0o0400 # Unix V7 synonym for S_IRUSR
S_IWRITE = 0o0200 # Unix V7 synonym for S_IWUSR
S_IEXEC = 0o0100 # Unix V7 synonym for S_IXUSR
S_IRWXU = 0o0700 # mask for owner permissions
S_IRUSR = 0o0400 # read by owner
S_IWUSR = 0o0200 # write by owner
S_IXUSR = 0o0100 # execute by owner
S_IRWXG = 0o0070 # mask for group permissions
S_IRGRP = 0o0040 # read by group
S_IWGRP = 0o0020 # write by group
S_IXGRP = 0o0010 # execute by group
S_IRWXO = 0o0007 # mask for others (not in group) permissions
S_IROTH = 0o0004 # read by others
S_IWOTH = 0o0002 # write by others
S_IXOTH = 0o0001 # execute by others
# Names for file flags
UF_NODUMP = 0x00000001 # do not dump file
UF_IMMUTABLE = 0x00000002 # file may not be changed
UF_APPEND = 0x00000004 # file may only be appended to
UF_OPAQUE = 0x00000008 # directory is opaque when viewed through a union stack
UF_NOUNLINK = 0x00000010 # file may not be renamed or deleted
UF_COMPRESSED = 0x00000020 # OS X: file is hfs-compressed
UF_HIDDEN = 0x00008000 # OS X: file should not be displayed
SF_ARCHIVED = 0x00010000 # file may be archived
SF_IMMUTABLE = 0x00020000 # file may not be changed
SF_APPEND = 0x00040000 # file may only be appended to
SF_NOUNLINK = 0x00100000 # file may not be renamed or deleted
SF_SNAPSHOT = 0x00200000 # file is a snapshot file
_filemode_table = (((S_IFLNK, "l"), (S_IFREG, "-"), (S_IFBLK, "b"),
(S_IFDIR, "d"), (S_IFCHR, "c"),
(S_IFIFO, "p")), ((S_IRUSR, "r"), ), ((S_IWUSR, "w"), ),
((S_IXUSR | S_ISUID, "s"), (S_ISUID, "S"),
(S_IXUSR, "x")), ((S_IRGRP, "r"), ), ((S_IWGRP, "w"), ),
((S_IXGRP | S_ISGID, "s"), (S_ISGID, "S"),
(S_IXGRP, "x")), ((S_IROTH, "r"), ), ((S_IWOTH, "w"), ),
((S_IXOTH | S_ISVTX, "t"), (S_ISVTX, "T"), (S_IXOTH, "x")))
def filemode(mode):
"""Convert a file's mode to a string of the form '-rwxrwxrwx'."""
perm = []
for table in _filemode_table:
for bit, char in table:
if mode & bit == bit:
perm.append(char)
break
else:
perm.append("-")
return "".join(perm)

79
micropython/time.py Normal file
View File

@@ -0,0 +1,79 @@
from utime import *
from micropython import const
_TS_YEAR = const(0)
_TS_MON = const(1)
_TS_MDAY = const(2)
_TS_HOUR = const(3)
_TS_MIN = const(4)
_TS_SEC = const(5)
_TS_WDAY = const(6)
_TS_YDAY = const(7)
_TS_ISDST = const(8)
_WDAY = const(("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"))
_MDAY = const(
(
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
)
)
def strftime(datefmt, ts):
from io import StringIO
fmtsp = False
ftime = StringIO()
for k in datefmt:
if fmtsp:
if k == "a":
ftime.write(_WDAY[ts[_TS_WDAY]][0:3])
elif k == "A":
ftime.write(_WDAY[ts[_TS_WDAY]])
elif k == "b":
ftime.write(_MDAY[ts[_TS_MON] - 1][0:3])
elif k == "B":
ftime.write(_MDAY[ts[_TS_MON] - 1])
elif k == "d":
ftime.write("%02d" % ts[_TS_MDAY])
elif k == "H":
ftime.write("%02d" % ts[_TS_HOUR])
elif k == "I":
ftime.write("%02d" % (ts[_TS_HOUR] % 12))
elif k == "j":
ftime.write("%03d" % ts[_TS_YDAY])
elif k == "m":
ftime.write("%02d" % ts[_TS_MON])
elif k == "M":
ftime.write("%02d" % ts[_TS_MIN])
elif k == "P":
ftime.write("AM" if ts[_TS_HOUR] < 12 else "PM")
elif k == "S":
ftime.write("%02d" % ts[_TS_SEC])
elif k == "w":
ftime.write(str(ts[_TS_WDAY]))
elif k == "y":
ftime.write("%02d" % (ts[_TS_YEAR] % 100))
elif k == "Y":
ftime.write(str(ts[_TS_YEAR]))
else:
ftime.write(k)
fmtsp = False
elif k == "%":
fmtsp = True
else:
ftime.write(k)
val = ftime.getvalue()
ftime.close()
return val

BIN
micropython/traceback.mpy Normal file

Binary file not shown.

View File

@@ -0,0 +1,16 @@
#!/bin/bash
while true; do
# Send request to modem and wait 5 seconds for data
echo -en "AT+QSPN;+CEREG=2;+CEREG?;+CEREG=0;+C5GREG=2;+C5GREG?;+C5GREG=0;+CSQ;+QENG=\"servingcell\";+QRSRP;+QCAINFO;+QNWPREFCFG=\"mode_pref\";+QTEMP\r\n" \
| microcom -t 3000 /dev/ttyOUT > /tmp/modemstatus.txt
if [ $? -eq 0 ]
then
# Parse
if [ -f /tmp/modemstatus.txt ]
then
/usrdata/simpleadmin/scripts/modemstatus_parse.sh
fi
fi
sleep 25 # Add a sleep to avoid CPU overload
done

31
scripts/doAT.py Normal file
View File

@@ -0,0 +1,31 @@
#!/usrdata/micropython/micropython
# Add the /usrdata/micropython directory to sys.path so we can find the external modules.
# TODO: Move external modules to lib?
# TODO: Recompile Micropython with a syspath set up for our use case.
import sys
# Remove the home directory from sys.path.
if "~/.micropython/lib" in sys.path:
sys.path.remove("~/.micropython/lib")
sys.path.append("/usrdata/micropython")
import serial
import uos
atcmd = sys.argv[1]
ser = serial.Serial("/dev/ttyOUT", baudrate=115200)
ser.write(atcmd + "\r\n")
uos.system("sleep 0.025s")
# wait for an OK
out=r''
while ser.in_waiting > 0:
out += ser.read(1)
if "OK" not in str(out):
print('Error NOT OK')
print(out.decode('utf-8'))
ser.close()

View File

@@ -0,0 +1,470 @@
#!/bin/bash
# Adapted to work with RJ45 / Quectel Board Dev
# Quectel AT Parsing Original source ROOter2203
# https://github.com/ofmodemsandmen/ROOterSource2203/blob/6636758b945ff16b6c5b54494de04b74b011c204/package/rooter/ext-rooter-basic/files/usr/lib/rooter/common/quecteldata.sh
#
rspr2rssi() {
echo ${RSCP} ${BW_N} | awk '{printf "%.0f\n", (($1+10*log(12*$2)/log(10)))}'
}
lte_bw() {
BW=$(echo $BW | grep -o "[0-5]\{1\}")
case $BW in
"0")
BW="1.4" ;;
"1")
BW="3" ;;
"2"|"3"|"4"|"5")
BW=$((($(echo $BW) - 1) * 5)) ;;
esac
}
nr_bw() {
BW=$(echo $BW | grep -o "[0-9]\{1,2\}")
case $BW in
"0"|"1"|"2"|"3"|"4"|"5")
BW=$((($(echo $BW) + 1) * 5)) ;;
"6"|"7"|"8"|"9"|"10"|"11"|"12")
BW=$((($(echo $BW) - 2) * 10)) ;;
"13")
BW="200" ;;
"14")
BW="400" ;;
esac
}
if [ ! -f /tmp/modemstatus.txt ]
then
/usrdata/simpleadmin/scripts/get_modem_data.py > /dev/null
fi
# Read File
OX=$(</tmp/modemstatus.txt)
OX=$(echo $OX | tr 'a-z' 'A-Z')
RSRP=""
RSRQ=""
CHANNEL="-"
ECIO="-"
RSCP="-"
ECIO1=" "
RSCP1=" "
MODE="-"
MODTYPE="-"
NETMODE="-"
LBAND="-"
PCI="-"
CTEMP="-"
SINR="-"
COPS="-"
COPS_MCC="-"
COPS_MNC="-"
CID=""
CID5=""
RAT=""
QSPN=$(echo $OX | grep -o '+QSPN: "[^"]*","[^"]*","[^"]*",[^"]*,"[^"]*"' | cut -c 8-)
PROVIDER=$(echo $QSPN | cut -d, -f1 | tr -d '"')
PROVIDER_ID=$(echo $QSPN | cut -d, -f5 | tr -d '"')
CSQ=$(echo $OX | grep -o "+CSQ: [0-9]\{1,2\}" | grep -o "[0-9]\{1,2\}")
if [ "$CSQ" = "99" ]; then
CSQ=""
fi
if [ -n "$CSQ" ]; then
CSQ_PER=$(($CSQ * 100/31))"%"
CSQ_RSSI=$((2 * CSQ - 113))" dBm"
else
CSQ="-"
CSQ_PER="-"
CSQ_RSSI="-"
fi
NR_NSA=$(echo $OX | grep -o "+QENG:[ ]\?\"NR5G-NSA\",")
NR_SA=$(echo $OX | grep -o "+QENG: \"SERVINGCELL\",[^,]\+,\"NR5G-SA\",\"[DFT]\{3\}\",")
if [ -n "$NR_NSA" ]; then
QENG=",,"$(echo $OX" " | grep -o "+QENG: \"LTE\".\+\"NR5G-NSA\"," | tr " " ",")
if [ -z "$QENG5" ]; then
QENG5=$(echo $OX | grep -o "+QENG:[ ]\?\"NR5G-NSA\",[0-9]\{3\},[0-9]\{2,3\},[0-9]\{1,5\},-[0-9]\{2,3\},[-0-9]\{1,3\},-[0-9]\{2,3\}")
if [ -n "$QENG5" ]; then
QENG5=$QENG5",,"
fi
fi
elif [ -n "$NR_SA" ]; then
QENG=$(echo $NR_SA | tr " " ",")
QENG5=$(echo $OX | grep -o "+QENG: \"SERVINGCELL\",[^,]\+,\"NR5G-SA\",\"[DFT]\{3\}\",[ 0-9]\{3,4\},[0-9]\{2,3\},[0-9A-F]\{1,10\},[0-9]\{1,5\},[0-9A-F]\{2,6\},[0-9]\{6,7\},[0-9]\{1,3\},[0-9]\{1,2\},-[0-9]\{2,5\},-[0-9]\{2,3\},[-0-9]\{1,3\}")
else
QENG=$(echo $OX" " | grep -o "+QENG: [^ ]\+ " | tr " " ",")
fi
QCA=$(echo $OX" " | grep -o "+QCAINFO: \"S[CS]\{2\}\".\+NWSCANMODE" | tr " " ",")
QNSM=$(echo $OX | grep -o "+QCFG: \"NWSCANMODE\",[0-9]")
QNWP=$(echo $OX | grep -o "+QNWPREFCFG: \"MODE_PREF\",[A-Z5:]\+" | cut -d, -f2)
QTEMP=$(echo $OX | grep -o "+QTEMP: [0-9]\{1,3\}")
if [ -z "$QTEMP" ]; then
QTEMP=$(echo $OX | grep -o "+QTEMP:[ ]\?\"XO[_-]THERM[_-][^,]\+,[\"]\?[0-9]\{1,3\}" | grep -o "[0-9]\{1,3\}")
fi
if [ -z "$QTEMP" ]; then
QTEMP=$(echo $OX | grep -o "+QTEMP:[ ]\?\"MDM-CORE-USR.\+[0-9]\{1,3\}\"" | cut -d\" -f4)
fi
if [ -z "$QTEMP" ]; then
QTEMP=$(echo $OX | grep -o "+QTEMP:[ ]\?\"MDMSS.\+[0-9]\{1,3\}\"" | cut -d\" -f4)
fi
if [ -n "$QTEMP" ]; then
CTEMP=$(echo $QTEMP | grep -o "[0-9]\{1,3\}")$(printf "\xc2\xb0")"C"
fi
RAT=$(echo $QENG | cut -d, -f4 | grep -o "[-A-Z5]\{3,7\}")
rm -f /tmp/modnetwork
case $RAT in
"GSM")
MODE="GSM"
;;
"WCDMA")
MODE="WCDMA"
CHANNEL=$(echo $QENG | cut -d, -f9)
RSCP=$(echo $QENG | cut -d, -f12)
RSCP="-"$(echo $RSCP | grep -o "[0-9]\{1,3\}")
ECIO=$(echo $QENG | cut -d, -f13)
ECIO="-"$(echo $ECIO | grep -o "[0-9]\{1,3\}")
;;
"LTE"|"CAT-M"|"CAT-NB")
MODE=$(echo $QENG | cut -d, -f5 | grep -o "[DFT]\{3\}")
if [ -n "$MODE" ]; then
MODE="$RAT $MODE"
else
MODE="$RAT"
fi
PCI=$(echo $QENG | cut -d, -f9)
CHANNEL=$(echo $QENG | cut -d, -f10)
LBAND=$(echo $QENG | cut -d, -f11 | grep -o "[0-9]\{1,3\}")
BW=$(echo $QENG | cut -d, -f12)
lte_bw
BWU=$BW
BW=$(echo $QENG | cut -d, -f13)
lte_bw
BWD=$BW
if [ -z "$BWD" ]; then
BWD="unknown"
fi
if [ -z "$BWU" ]; then
BWU="unknown"
fi
if [ -n "$LBAND" ]; then
LBAND="B"$LBAND" (Bandwidth $BWD MHz Down | $BWU MHz Up)"
fi
RSRP=$(echo $QENG | cut -d, -f15 | grep -o "[0-9]\{1,3\}")
if [ -n "$RSRP" ]; then
RSCP="-"$RSRP
RSRPLTE=$RSCP
fi
RSRQ=$(echo $QENG | cut -d, -f16 | grep -o "[0-9]\{1,3\}")
if [ -n "$RSRQ" ]; then
ECIO="-"$RSRQ
fi
RSSI=$(echo $QENG | cut -d, -f17 | grep -o "\-[0-9]\{1,3\}")
if [ -n "$RSSI" ]; then
CSQ_RSSI=$RSSI" dBm"
fi
SINRR=$(echo $QENG | cut -d, -f18 | grep -o "[0-9]\{1,3\}")
if [ -n "$SINRR" ]; then
if [ $SINRR -le 25 ]; then
SINR=$((($(echo $SINRR) * 2) -20))" dB"
fi
fi
if [ -n "$(echo $QENG | cut -d, -f21)" ]; then
CQI=$(echo $QENG | cut -d, -f19 | grep "^[0-9]\+$")
if [ -n "$SINR" -a -n "$CQI" -a "$CQI" != "0" ]; then
SINR=$SINR" (CQI $CQI)"
fi
fi
if [ -n "$NR_NSA" ]; then
MODE="LTE/NR EN-DC"
echo "0" > /tmp/modnetwork
if [ -n "$QENG5" ] && [ -n "$LBAND" ] && [ "$RSCP" != "-" ] && [ "$ECIO" != "-" ]; then
PCI="$PCI, "$(echo $QENG5 | cut -d, -f4)
SCHV=$(echo $QENG5 | cut -d, -f8)
SLBV=$(echo $QENG5 | cut -d, -f9)
BW=$(echo $QENG5 | cut -d, -f10 | grep -o "[0-9]\{1,3\}")
if [ -n "$SLBV" ]; then
LBAND=$LBAND"<br />n"$SLBV
if [ -n "$BW" ]; then
nr_bw
LBAND=$LBAND" (Bandwidth $BW MHz)"
fi
if [ "$SCHV" -ge 123400 ]; then
CHANNEL=$CHANNEL", "$SCHV
else
CHANNEL=$CHANNEL", -"
fi
else
LBAND=$LBAND"<br />nxx (unknown NR5G band)"
CHANNEL=$CHANNEL", -"
fi
RSCP=$RSCP" dBm<br />"$(echo $QENG5 | cut -d, -f5)
SINRR=$(echo $QENG5 | cut -d, -f6 | grep -o "[0-9]\{1,3\}")
if [ -n "$SINRR" ]; then
if [ $SINRR -le 30 ]; then
SINR=$SINR"<br />"$((($(echo $SINRR) * 2) -20))" dB"
fi
fi
ECIO=$ECIO" (4G) dB<br />"$(echo $QENG5 | cut -d, -f7)" (5G) "
fi
fi
if [ -z "$LBAND" ]; then
LBAND="-"
else
if [ -n "$QCA" ]; then
QCA=$(echo $QCA | grep -o "\"S[CS]\{2\}\"[-0-9A-Z,\"]\+")
for QCAL in $(echo "$QCA"); do
if [ $(echo "$QCAL" | cut -d, -f7) = "2" ]; then
SCHV=$(echo $QCAL | cut -d, -f2 | grep -o "[0-9]\+")
SRATP="B"
if [ -n "$SCHV" ]; then
CHANNEL="$CHANNEL, $SCHV"
if [ "$SCHV" -gt 123400 ]; then
SRATP="n"
fi
fi
SLBV=$(echo $QCAL | cut -d, -f6 | grep -o "[0-9]\{1,2\}")
if [ -n "$SLBV" ]; then
LBAND=$LBAND"<br />"$SRATP$SLBV
BWD=$(echo $QCAL | cut -d, -f3 | grep -o "[0-9]\{1,3\}")
if [ -n "$BWD" ]; then
UPDOWN=$(echo $QCAL | cut -d, -f13)
case "$UPDOWN" in
"UL" )
CATYPE="CA"$(printf "\xe2\x86\x91") ;;
"DL" )
CATYPE="CA"$(printf "\xe2\x86\x93") ;;
* )
CATYPE="CA" ;;
esac
if [ $BWD -gt 14 ]; then
LBAND=$LBAND" ("$CATYPE", Bandwidth "$(($(echo $BWD) / 5))" MHz)"
else
LBAND=$LBAND" ("$CATYPE", Bandwidth 1.4 MHz)"
fi
fi
LBAND=$LBAND
fi
PCI="$PCI, "$(echo $QCAL | cut -d, -f8)
fi
done
fi
fi
if [ $RAT = "CAT-M" ] || [ $RAT = "CAT-NB" ]; then
LBAND="B$(echo $QENG | cut -d, -f11) ($RAT)"
fi
;;
"NR5G-SA")
MODE="NR5G-SA"
echo "0" > /tmp/modnetwork
if [ -n "$QENG5" ]; then
MODE="$RAT $(echo $QENG5 | cut -d, -f4)"
PCI=$(echo $QENG5 | cut -d, -f8)
CHANNEL=$(echo $QENG5 | cut -d, -f10)
LBAND=$(echo $QENG5 | cut -d, -f11)
BW=$(echo $QENG5 | cut -d, -f12)
LBAND="n"$LBAND" (Bandwidth $BW MHz)"
RSCP=$(echo $QENG5 | cut -d, -f13)
ECIO=$(echo $QENG5 | cut -d, -f14)
if [ "$CSQ_PER" = "-" ]; then
BW_N=($BW * 5)
RSSI=$(rspr2rssi)
CSQ_PER=$((100 - (($RSSI + 51) * 100/-62)))"%"
CSQ=$((($RSSI + 113) / 2))
CSQ_RSSI=$RSSI" dBm"
fi
SINRR=$(echo $QENG5 | cut -d, -f15 | grep -o "[0-9]\{1,3\}")
if [ -n "$SINRR" ]; then
if [ $SINRR -le 30 ]; then
SINR=$((($(echo $SINRR) * 2) -20))" dB"
fi
fi
fi
;;
esac
QRSRP=$(echo "$OX" | grep -o "+QRSRP:[^,]\+,-[0-9]\{1,5\},-[0-9]\{1,5\},-[0-9]\{1,5\}[^ ]*")
if [ -n "$QRSRP" ] && [ "$RAT" != "WCDMA" ]; then
QRSRP1=$(echo $QRSRP | cut -d, -f1 | grep -o "[-0-9]\+")
QRSRP2=$(echo $QRSRP | cut -d, -f2)
QRSRP3=$(echo $QRSRP | cut -d, -f3)
QRSRP4=$(echo $QRSRP | cut -d, -f4)
QRSRPtype=$(echo $QRSRP | cut -d, -f5)
if [ "$QRSRPtype" == "NR5G" ]; then
if [ -n "$NR_SA" ]; then
RSCP=$QRSRP1
if [ -n "$QRSRP2" -a "$QRSRP2" != "-32768" ]; then
RSCP1="RxD "$QRSRP2
fi
if [ -n "$QRSRP3" -a "$QRSRP3" != "-32768" -a "$QRSRP3" != "-44" ]; then
RSCP=$RSCP" dBm<br />"$QRSRP3
fi
if [ -n "$QRSRP4" -a "$QRSRP4" != "-32768" -a "$QRSRP4" != "-44" ]; then
RSCP1="RxD "$QRSRP4
fi
else
RSCP=$RSRPLTE
if [ -n "$QRSRP1" -a "$QRSRP1" != "-32768" -a "$QRSRP1" != "-44" ]; then
RSCP=$RSCP" (4G) dBm<br />"$QRSRP1
if [ -n "$QRSRP2" -a "$QRSRP2" != "-32768" -a "$QRSRP2" != "-44" ]; then
RSCP="$RSCP, $QRSRP2"
if [ -n "$QRSRP3" -a "$QRSRP3" != "-32768" -a "$QRSRP3" != "-44" ]; then
RSCP="$RSCP, $QRSRP3"
if [ -n "$QRSRP4" -a "$QRSRP4" != "-32768" -a "$QRSRP4" != "-44" ]; then
RSCP="$RSCP, $QRSRP4"
fi
fi
RSCP=$RSCP" (5G) "
fi
fi
fi
elif [ "$QRSRP2$QRSRP3$QRSRP4" != "-44-44-44" -a -z "$QENG5" ]; then
RSCP=$QRSRP1
if [ "$QRSRP3$QRSRP4" == "-140-140" -o "$QRSRP3$QRSRP4" == "-44-44" -o "$QRSRP3$QRSRP4" == "-32768-32768" ]; then
RSCP1="RxD "$(echo $QRSRP | cut -d, -f2)
else
RSCP=$RSCP" dBm (RxD "$QRSRP2" dBm)<br />"$QRSRP3
RSCP1="RxD "$QRSRP4
fi
fi
fi
QNSM=$(echo "$QNSM" | grep -o "[0-9]")
if [ -n "$QNSM" ]; then
MODTYPE="6"
case $QNSM in
"0" )
NETMODE="1" ;;
"1" )
NETMODE="3" ;;
"2"|"5" )
NETMODE="5" ;;
"3" )
NETMODE="7" ;;
esac
fi
if [ -n "$QNWP" ]; then
MODTYPE="6"
case $QNWP in
"AUTO" )
NETMODE="1" ;;
"WCDMA" )
NETMODE="5" ;;
"LTE" )
NETMODE="7" ;;
"LTE:NR5G" )
NETMODE="8" ;;
"NR5G" )
NETMODE="9" ;;
esac
fi
OX=$(echo "${OX//[ \"]/}")
REGV=$(echo "$OX" | grep -o "+C5GREG:2,[0-9],[A-F0-9]\{2,6\},[A-F0-9]\{5,10\},[0-9]\{1,2\}")
if [ -n "$REGV" ]; then
LAC5=$(echo "$REGV" | cut -d, -f3)
LAC5=$LAC5" ($(printf "%d" 0x$LAC5))"
CID5=$(echo "$REGV" | cut -d, -f4)
CID5L=$(printf "%010X" 0x$CID5)
RNC5=${CID5L:1:6}
RNC5=$RNC5" ($(printf "%d" 0x$RNC5))"
CID5=${CID5L:7:3}
CID5="Short $(printf "%X" 0x$CID5) ($(printf "%d" 0x$CID5)), Long $(printf "%X" 0x$CID5L) ($(printf "%d" 0x$CID5L))"
RAT=$(echo "$REGV" | cut -d, -f5)
fi
REGV=$(echo "$OX" | grep -o "+CEREG:2,[0-9],[A-F0-9]\{2,4\},[A-F0-9]\{5,8\}")
REGFMT="3GPP"
if [ -z "$REGV" ]; then
REGV=$(echo "$OX" | grep -o "+CEREG:2,[0-9],[A-F0-9]\{2,4\},[A-F0-9]\{1,3\},[A-F0-9]\{5,8\}")
REGFMT="SW"
fi
if [ -n "$REGV" ]; then
LAC=$(echo "$REGV" | cut -d, -f3)
LAC=$(printf "%04X" 0x$LAC)" ($(printf "%d" 0x$LAC))"
if [ $REGFMT = "3GPP" ]; then
CID=$(echo "$REGV" | cut -d, -f4)
else
CID=$(echo "$REGV" | cut -d, -f5)
fi
CIDL=$(printf "%08X" 0x$CID)
RNC=${CIDL:1:5}
RNC=$RNC" ($(printf "%d" 0x$RNC))"
CID=${CIDL:6:2}
CID="Short $(printf "%X" 0x$CID) ($(printf "%d" 0x$CID)), Long $(printf "%X" 0x$CIDL) ($(printf "%d" 0x$CIDL))"
else
REGV=$(echo "$OX" | grep -o "+CREG:2,[0-9],[A-F0-9]\{2,4\},[A-F0-9]\{2,8\}")
if [ -n "$REGV" ]; then
LAC=$(echo "$REGV" | cut -d, -f3)
CID=$(echo "$REGV" | cut -d, -f4)
if [ ${#CID} -gt 4 ]; then
LAC=$(printf "%04X" 0x$LAC)" ($(printf "%d" 0x$LAC))"
CIDL=$(printf "%08X" 0x$CID)
RNC=${CIDL:1:3}
CID=${CIDL:4:4}
CID="Short $(printf "%X" 0x$CID) ($(printf "%d" 0x$CID)), Long $(printf "%X" 0x$CIDL) ($(printf "%d" 0x$CIDL))"
else
LAC=""
fi
else
LAC=""
fi
fi
REGSTAT=$(echo "$REGV" | cut -d, -f2)
if [ "$REGSTAT" == "5" -a "$COPS" != "-" ]; then
COPS_MNC=$COPS_MNC" (Roaming)"
fi
if [ -n "$CID" -a -n "$CID5" ] && [ "$RAT" == "13" -o "$RAT" == "10" ]; then
LAC="4G $LAC, 5G $LAC5"
CID="4G $CID<br />5G $CID5"
RNC="4G $RNC, 5G $RNC5"
elif [ -n "$CID5" ]; then
LAC=$LAC5
CID=$CID5
RNC=$RNC5
fi
if [ -z "$LAC" ]; then
LAC="-"
CID="-"
RNC="-"
fi
LUPDATE=$(date +%s)
rm -fR /tmp/signal.txt
MODEZ=$(echo $MODE | tr -d '"')
{
echo 'PROVIDER="'"$PROVIDER"'"'
echo 'CSQ="'"$CSQ"'"'
echo 'CSQ_PER="'"$CSQ_PER"'"'
echo 'CSQ_RSSI="'"$CSQ_RSSI"'"'
echo 'ECIO="'"$ECIO"'"'
echo 'RSCP="'"$RSCP"'"'
echo 'ECIO1="'"$ECIO1"'"'
echo 'RSCP1="'"$RSCP1"'"'
echo 'MODE="'"$MODEZ"'"'
echo 'MODTYPE="'"$MODTYPE"'"'
echo 'NETMODE="'"$NETMODE"'"'
echo 'CHANNEL="'"$CHANNEL"'"'
echo 'LBAND="'"$LBAND"'"'
echo 'PCI="'"$PCI"'"'
echo 'TEMP="'"$CTEMP"'"'
echo 'SINR="'"$SINR"'"'
echo 'LASTUPDATE="'"$LUPDATE"'"'
echo 'COPS="'"$COPS"'"'
echo 'COPS_MCC="'"$COPS_MCC"'"'
echo 'COPS_MNC="'"$COPS_MNC"'"'
echo 'LAC="'"$LAC"'"'
echo 'LAC_NUM="'""'"'
echo 'CID="'"$CID"'"'
echo 'CID_NUM="'""'"'
echo 'RNC="'"$RNC"'"'
echo 'RNC_NUM="'""'"'
} > /tmp/signal.txt
# Pregenerate JSON File
/usrdata/simpleadmin/scripts/tojson.sh /tmp/signal.txt > /tmp/modemstatus.json

22
scripts/tojson.sh Normal file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
# sarav (hello@grity.com)
# convert key=value to json
# Created at Gritfy ( Devops Junction )
#
file_name=$1
last_line=$(wc -l < $file_name)
current_line=0
echo "{"
while read line
do
current_line=$(($current_line + 1))
if [[ $current_line -ne $last_line ]]; then
[ -z "$line" ] && continue
echo $line|awk -F'=' '{ print " \""$1"\" : "$2","}'|grep -iv '\"#'
else
echo $line|awk -F'=' '{ print " \""$1"\" : "$2""}'|grep -iv '\"#'
fi
done < $file_name
echo "}"

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Simpleadmin service to generate status from modem
Requires=socat-smd11.service
[Service]
ExecStart=/usrdata/simpleadmin/scripts/build_modem_status
Restart=always
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,11 @@
[Unit]
Description=SimpleAdmin httpd service
After=network.target
[Service]
Type=simple
ExecStart=/usr/sbin/httpd -f -h /usrdata/simpleadmin/www -p 8080
ExecStop=/bin/kill -WINCH ${MAINPID}
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,12 @@
[Unit]
Description=TTL Override
After=ql-netd.service
DefaultDependencies=no
[Service]
Type=oneshot
ExecStart=/usrdata/simpleadmin/ttl/ttl-override start
User=root
[Install]
WantedBy=multi-user.target

53
ttl/ttl-override Normal file
View File

@@ -0,0 +1,53 @@
#! /bin/bash
# Adapted from https://github.com/natecarlson/quectel-rgmii-configuration-notes/blob/main/files/ttl-override
# Uses ttlvalue file to read what ttl should be set to
if [ -f /usrdata/simpleadmin/ttl/ttlvalue ];
then
ttlfile=$(</usrdata/simpleadmin/ttl/ttlvalue)
TTLVALUE=$(echo $ttlfile | grep -o "[0-9]\{1,3\}")
if [ -z "${TTLVALUE}" ]; then
echo "Couldnt get proper ttl value from file" >&2
exit 1
fi
else
# Couldnt find ttlvalue file, lets generate one with 0 ttlvalue (0 = disabled)
touch /usrdata/simpleadmin/ttl/ttlvalue && echo '0' > /usrdata/simpleadmin/ttl/ttlvalue
exit 1
fi
case "$1" in
start)
if (( $TTLVALUE > 0 )); then
echo "Adding TTL override rules: "
iptables -t mangle -I POSTROUTING -o rmnet+ -j TTL --ttl-set ${TTLVALUE}
ip6tables -t mangle -I POSTROUTING -o rmnet+ -j HL --hl-set ${TTLVALUE}
else
echo "TTLVALUE set to 0, nothing to do..."
fi
echo "done"
;;
stop)
if (( $TTLVALUE > 0 )); then
echo "Removing TTL override rules: "
iptables -t mangle -D POSTROUTING -o rmnet+ -j TTL --ttl-set ${TTLVALUE} &>/dev/null || true
ip6tables -t mangle -D POSTROUTING -o rmnet+ -j HL --hl-set ${TTLVALUE} &>/dev/null || true
else
echo "TTLVALUE set to 0, nothing to do..."
fi
echo "done"
;;
restart)
$0 stop
$0 start
;;
*)
echo "Usage ttl-override { start | stop | restart }" >&2
exit 1
;;
esac
exit 0

1
ttl/ttlvalue Normal file
View File

@@ -0,0 +1 @@
0

49
uninstall_on_modem.sh Normal file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
#
# Removes SimpleAdmin
#
read -p "Do you want to uninstall SimpleAdmin (yes/no) " yn
case $yn in
yes ) echo ok, we will proceed;;
no ) echo exiting...;
exit;;
* ) echo invalid response;
exit 1;;
esac
# ExecStop
systemctl stop simpleadmin_generate_status.timer
systemctl stop simpleadmin_generate_status
systemctl stop simpleadmin_httpd
systemctl stop ttl-override
#Remove from /usrdata
rm -rf /usrdata/simpleadmin
# Remount
mount -o remount,rw /
# Copy systemd init files & reload
#remove links
rm /lib/systemd/system/multi-user.target.wants/simpleadmin_httpd.service
rm /lib/systemd/system/multi-user.target.wants/simpleadmin_generate_status.service
rm /lib/systemd/system/timers.target.wants/simpleadmin_generate_status.timer
rm /lib/systemd/system/multi-user.target.wants/ttl-override.service
#remove files
rm /lib/systemd/system/simpleadmin_generate_status.timer
rm /lib/systemd/system/simpleadmin_httpd.service
rm /lib/systemd/system/simpleadmin_generate_status.service
rm /lib/systemd/system/ttl-override.service
systemctl daemon-reload
# Link systemd files
# Remount readonly
mount -o remount,ro /

123
www/atcommander.html Normal file
View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quectel Simple Admin</title>
<script src="/js/alpinejs.min.js" defer></script>
<link rel="stylesheet" href="/css/bulma.css">
<link rel="stylesheet" type="text/css" href="/css/admin.css">
</head>
<body>
<!-- START NAV -->
<nav class="navbar is-white" x-data="{ isOpen: false }">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item brand-text" href="/">
Quectel Simple Admin
</a>
<a role="button" class="navbar-burger burger" @click="isOpen = !isOpen">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navMenu" class="navbar-menu" :class="isOpen ? 'is-active' : ''">
<div class="navbar-start">
<a class="navbar-item" href="/">
Home
</a>
<a class="navbar-item" href="/atcommander.html">
ATI Commander
</a>
<a class="navbar-item" href="/ttl.html">
TTL Enabler
</a>
</div>
</div>
</div>
</nav>
<!-- END NAV -->
<div class="container" x-data="atCommands()">
<div class="columns">
<div class="column is-12">
<div class="columns">
<div class="column is-4">
<div class="card">
<header class="card-header">
<p class="card-header-title">
AT Command
</p>
</header>
<div class="card-content">
<div class="content">
<div class="field">
<label class="label">AT Command</label>
<div class="control">
<input class="input" type="text" placeholder="ATI" x-model="atcmd">
</div>
</div>
<div class="field">
<p class="control">
<button class="button is-success" @click="sendAtCommand()">
Send AT Command
</button>
</p>
</div>
</div>
</div>
</div>
</div>
<div class="column is-8">
<div class="card">
<header class="card-header">
<p class="card-header-title">
ATI Response
</p>
</header>
<div class="card-content">
<div class="content">
<textarea class="textarea" placeholder="ATI Responses Will Appear Here" rows="10"
x-text="atCommandResponse"></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function atCommands() {
return {
isLoading: false,
atcmd: null,
atCommandResponse: null,
sendAtCommand() {
fetch('/cgi-bin/get_atcommand?' + new URLSearchParams({
atcmd: this.atcmd,
}))
.then((res) => {
return res.text();
})
.then((data) => {
this.atCommandResponse = data;
this.isLoading = false;
})
},
}
}
</script>
</body>
</html>

30
www/cgi-bin/get_atcommand Normal file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
QUERY_STRING=$(echo "${QUERY_STRING}" | sed 's/;//g')
function urldecode() { : "${*//+/ }"; echo -e "${_//%/\\x}"; }
if [ "${QUERY_STRING}" ]; then
export IFS="&"
for cmd in ${QUERY_STRING}; do
if [ "$(echo $cmd | grep '=')" ]; then
key=$(echo $cmd | awk -F '=' '{print $1}')
value=$(echo $cmd | awk -F '=' '{print $2}')
eval $key=$value
fi
done
fi
MYATCMD=$(printf '%b\n' "${atcmd//%/\\x}")
if [ -n "${MYATCMD}" ]; then
x=$(urldecode "$atcmd")
runcmd=$(echo -en "$x\r\n" | microcom -t 2000 /dev/ttyOUT)
# runcmd=$(/usrdata/simpleadmin/scripts/doAT.py "$MYATCMD")
fi
echo "Content-type: text/plain"
echo $x
echo ""
echo $runcmd

13
www/cgi-bin/get_csq Normal file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
if [ ! -f /tmp/modemstatus.json ]
then
/usrdata/simpleadmin/scripts/modemstatus_parse.sh > /dev/null
fi
runcmd=$(</tmp/modemstatus.json)
echo "Content-type: text/json"
echo ""
cat <<EOT
$runcmd

View File

@@ -0,0 +1,19 @@
#!/bin/bash
# Check iptables for ttlvalue
ttlvalue=$(iptables -t mangle -vnL | grep TTL | awk '{print $13}')
ttlenabled=true;
# Set Variables
if [ -z "${ttlvalue}" ]; then
ttlvalue=0
ttlenabled=false
fi
echo "Content-type: text/json"
echo ""
cat <<EOT
{
"isEnabled": $ttlenabled,
"ttl": $ttlvalue
}

61
www/cgi-bin/set_ttl Normal file
View File

@@ -0,0 +1,61 @@
#!/bin/bash
# Get query
QUERY_STRING=$(echo "${QUERY_STRING}" | sed 's/;//g')
if [ "${QUERY_STRING}" ]; then
export IFS="&"
for cmd in ${QUERY_STRING}; do
if [ "$(echo $cmd | grep '=')" ]; then
key=$(echo $cmd | awk -F '=' '{print $1}')
value=$(echo $cmd | awk -F '=' '{print $2}')
eval $key=$value
fi
done
fi
setTTL=$(printf '%b\n' "${ttlvalue//%/\\x}")
if [ -n "${setTTL}" ]; then
# Stop Service To Remove Rules
/usrdata/simpleadmin/ttl/ttl-override stop
# Check iptables is still set
ttlcheck=$(iptables -t mangle -vnL | grep TTL | awk '{print $13}')
# If TTL is still set manually remove values
if [ !-z "${ttlcheck}" ]; then
iptables -t mangle -D POSTROUTING -o rmnet+ -j TTL --ttl-set ${ttlcheck} &>/dev/null || true
ip6tables -t mangle -D POSTROUTING -o rmnet+ -j HL --hl-set ${ttlcheck} &>/dev/null || true
fi
# Echo TTL to file
echo $setTTL > /usrdata/simpleadmin/ttl/ttlvalue
# Set Start Service
/usrdata/simpleadmin/ttl/ttl-override start
fi
# Check iptables for ttlvalue
ttlvalue=$(iptables -t mangle -vnL | grep TTL | awk '{print $13}')
ttlenabled=true;
# Set Variables
if [ -z "${ttlvalue}" ]; then
ttlvalue=0
ttlenabled=false
fi
echo "Content-type: text/json"
echo ""
cat <<EOT
{
"isEnabled": $ttlenabled,
"ttl": $ttlvalue
}

86
www/css/admin.css Normal file
View File

@@ -0,0 +1,86 @@
:root{
::-webkit-scrollbar{height:10px;width:10px}::-webkit-scrollbar-track{background:#efefef;border-radius:6px}::-webkit-scrollbar-thumb{background:#d5d5d5;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:#c4c4c4}
}
html, body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
font-size: 16px;
line-height: 1.5;
height: 100%;
background: #ECF0F3;
}
nav.navbar {
border-top: 4px solid #276cda;
margin-bottom: 1rem;
}
.navbar-item.brand-text {
font-weight: 300;
}
.navbar-item, .navbar-link {
font-size: 14px;
font-weight: 700;
}
.columns {
width: 100%;
height: 100%;
margin-left: 0;
}
.menu-label {
color: #8F99A3;
letter-spacing: 1.3;
font-weight: 700;
}
.menu-list a {
color: #0F1D38;
font-size: 14px;
font-weight: 700;
}
.menu-list a:hover {
background-color: transparent;
color: #276cda;
}
.menu-list a.is-active {
background-color: transparent;
color: #276cda;
font-weight: 700;
}
.card {
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.18);
margin-bottom: 2rem;
}
.card-header-title {
color: #8F99A3;
font-weight: 400;
}
.info-tiles {
margin: 1rem 0;
}
.info-tiles .subtitle {
font-weight: 300;
color: #8F99A3;
}
.hero.welcome.is-info {
background: #36D1DC;
background: -webkit-linear-gradient(to right, #5B86E5, #36D1DC);
background: linear-gradient(to right, #5B86E5, #36D1DC);
}
.hero.welcome .title, .hero.welcome .subtitle {
color: hsl(192, 17%, 99%);
}
.card .content {
font-size: 14px;
}
.card-footer-item {
font-size: 14px;
font-weight: 700;
color: #8F99A3;
}
.card-footer-item:hover {
}
.card-table .table {
margin-bottom: 0;
}
.events-card .card-table {
height: 330px;
overflow-y: scroll;
}

1
www/css/bulma.css vendored Normal file

File diff suppressed because one or more lines are too long

232
www/index.html Normal file
View File

@@ -0,0 +1,232 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quectel Simple Admin</title>
<script src="/js/alpinejs.min.js" defer></script>
<link rel="stylesheet" href="/css/bulma.css">
<link rel="stylesheet" type="text/css" href="/css/admin.css">
</head>
<body>
<!-- START NAV -->
<nav class="navbar is-white" x-data="{ isOpen: false }">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item brand-text" href="/">
Quectel Simple Admin
</a>
<a role="button" class="navbar-burger burger" @click="isOpen = !isOpen">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navMenu" class="navbar-menu" :class="isOpen ? 'is-active' : ''">
<div class="navbar-start">
<a class="navbar-item" href="/">
Home
</a>
<a class="navbar-item" href="/atcommander.html">
ATI Commander
</a>
<a class="navbar-item" href="/ttl.html">
TTL Enabler
</a>
</div>
</div>
</div>
</nav>
<!-- END NAV -->
<div class="container">
<div class="columns">
<div class="column is-12" x-data="getSignalData()" x-init="init()">
<section class="hero is-info welcome is-small">
<div class="hero-body">
<div class="container">
<h1 class="title">
Welcome to Quectel RJ45 Board Simple Admin
</h1>
<h2 class="subtitle">
Data Updated: <span x-text="lastUpdate"></span>
</h2>
</div>
</div>
</section>
<section class="info-tiles">
<div class="tile is-ancestor has-text-centered">
<div class="tile is-parent">
<article class="tile is-child box">
<p class="title" x-text="csqData.MODE"></p>
<p class="subtitle">Network</p>
</article>
</div>
<div class="tile is-parent">
<article class="tile is-child box">
<p class="title" x-text="csqData.CSQ_PER"></p>
<p class="subtitle">Signal Strength</p>
</article>
</div>
<div class="tile is-parent">
<article class="tile is-child box">
<p class="title" x-text="csqData.TEMP"></p>
<p class="subtitle">Modem Temperature</p>
</article>
</div>
<div class="tile is-parent">
<article class="tile is-child box">
<p class="title" x-html="csqData.LBAND"></p>
<p class="subtitle">Band</p>
</article>
</div>
</div>
</section>
<div class="columns">
<div class="column is-6">
<div class="card events-card">
<header class="card-header">
<p class="card-header-title">
Signal Information
</p>
</header>
<div class="card-table">
<div class="content">
<table class="table is-fullwidth is-striped">
<tbody>
<tr>
<th>Provider</th>
<td x-text="csqData.PROVIDER">
</td>
</tr>
<tr>
<th>CSQ</th>
<td x-text="csqData.CSQ">
</td>
</tr>
<tr>
<th>Signal Strength</th>
<td x-text="csqData.CSQ_PER">
</td>
</tr>
<tr>
<th>RSSI</th>
<td x-text="csqData.CSQ_RSSI">
</td>
</tr>
<tr>
<th>ECIO<sup>3G</sup>/RSRQ<sup>4G</sup>/SS_RSRQ<sup>5G</sup></th>
<td x-html="csqData.ECIO">
</td>
</tr>
<tr>
<th>RSCP<sup>3G</sup>/RSRP<sup>4G</sup>/SS_RSRP<sup>5G</sup></th>
<td x-html="csqData.RSCP">
</td>
</tr>
<tr>
<th>SINR</th>
<td x-html="csqData.SINR">
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="column is-6">
<div class="card events-card">
<header class="card-header">
<p class="card-header-title">
Cell Information
</p>
</header>
<div class="card-table">
<div class="content">
<table class="table is-fullwidth is-striped">
<tbody>
<tr>
<th>MCC MNC</th>
<td>
<span x-text="csqData.COPS_MCC"></span>
/
<span x-text="csqData.COPS_MNC"></span>
</td>
</tr>
<tr>
<th>RNC<sup>3G</sup>/eNB ID<sup>4G/5G</sup></th>
<td>
<span x-text="csqData.RNC"></span>
<span x-text="csqData.RNC_NUM"></span>
</td>
</tr>
<tr>
<th>Lag<sup>3G</sup>/TAC<sup>4G/5G</sup></th>
<td>
<span x-text="csqData.LAC"></span>
<span x-text="csqData.LAC_NUM"></span>
</td>
</tr>
<tr>
<th>Cell ID</th>
<td x-text="csqData.CID">
</td>
</tr>
<tr>
<th>Band</th>
<td x-html="csqData.LBAND">
</td>
</tr>
<tr>
<th>Channel</th>
<td x-text="csqData.CHANNEL">
</td>
</tr>
<tr>
<th>PCI</th>
<td x-text="csqData.PCI">
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function getSignalData() {
return {
csqData: {},
lastUpdate: new Date().toLocaleString(),
getcsq() {
fetch('/cgi-bin/get_csq')
.then((res) => res.json())
.then((data) => {
this.csqData = data;
this.lastUpdate = new Date(data.LASTUPDATE * 1000).toLocaleString();
});
},
init() {
this.getcsq();
setInterval(() => {
this.getcsq();
}, 30000);
}
}
}
</script>
</body>
</html>

5
www/js/alpinejs.min.js vendored Normal file

File diff suppressed because one or more lines are too long

134
www/ttl.html Normal file
View File

@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quectel Simple Admin</title>
<script src="/js/alpinejs.min.js" defer></script>
<link rel="stylesheet" href="/css/bulma.css">
<link rel="stylesheet" type="text/css" href="/css/admin.css">
</head>
<body>
<!-- START NAV -->
<nav class="navbar is-white" x-data="{ isOpen: false }">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item brand-text" href="/">
Quectel Simple Admin
</a>
<a role="button" class="navbar-burger burger" @click="isOpen = !isOpen">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navMenu" class="navbar-menu" :class="isOpen ? 'is-active' : ''">
<div class="navbar-start">
<a class="navbar-item" href="/">
Home
</a>
<a class="navbar-item" href="/atcommander.html">
ATI Commander
</a>
<a class="navbar-item" href="/ttl.html">
TTL Enabler
</a>
</div>
</div>
</div>
</nav>
<!-- END NAV -->
<div class="container" x-data="ttlCommands()" x-init="init">
<div class="columns">
<div class="column is-12">
<div class="columns">
<div class="column is-8">
<div class="card">
<header class="card-header">
<p class="card-header-title">
TTL Enabler
</p>
</header>
<div class="card-content">
<div class="content">
<p>
<h2>TTL Status</h2> <br>
TTL is <span class="tag is-large" :class="ttldata.isEnabled ? 'is-success' : 'is-danger'" x-text="ttldata.isEnabled == true ? 'ON' : 'OFF'"></span>
<br />
TTL Set to <span x-text="ttldata.ttl"></span>
</p>
<div class="field">
<label class="label">Set TTL</label>
<div class="control">
<input class="input" type="number" placeholder="64"
x-model="newTTL">
</div>
</div>
<div class="field">
<div class="control">
<button class="button is-link" @click="setTTL()">Set TTL</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="column is-4">
<div class="card">
<header class="card-header">
<p class="card-header-title">
Common TTL For Providers
</p>
</header>
<div class="card-content">
<div class="content">
<ul>
<li>Magenta: 65</li>
<li>Red: 88</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function ttlCommands() {
return {
isLoading: false,
ttldata: null,
newTTL: 0,
init() {
fetch('/cgi-bin/get_ttl_status')
.then((res) => res.json())
.then((data) => {
this.ttldata = data
});
},
setTTL() {
fetch('/cgi-bin/set_ttl?' + new URLSearchParams({
ttlvalue: this.newTTL,
}))
.then((res) => res.json())
.then((data) => {
this.ttldata = data
})
}
}
}
</script>
</body>