├── .gitignore ├── README.md ├── at_telnet_daemon ├── README.md ├── at-telnet │ ├── modem-multiclient.py │ ├── picocom │ ├── socat-armel-static │ └── systemd_units │ │ ├── at-telnet-daemon.service │ │ ├── socat-smd11-from-ttyIN.service │ │ ├── socat-smd11-to-ttyIN.service │ │ └── socat-smd11.service └── micropython │ ├── errno.py │ ├── fcntl.py │ ├── ffilib.py │ ├── logging.py │ ├── micropython │ ├── os_compat.py │ ├── serial.py │ ├── stat.py │ ├── time.py │ └── traceback.mpy └── quectel_eth_at_client ├── README.md ├── direct-port.py ├── quectel_reference_c ├── .gitignore └── RGMII_AT_Client.c └── quectel_rgmii_at_client.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Quectel Modem Remote AT Command Support 2 | ======================================= 3 | 4 | This repo was originally just for the ETH+AT command client for Quectel modems. I never got it working right for all platforms, and I wanted something cleaner anyways.. so I build a daemon that just lets you telnet to port 5000 on the modem and do whatever you want. However, both options are still available. 5 | 6 | * Original ETH+AT Python client: [https://github.com/natecarlson/quectel-rgmii-at-command-client/tree/main/quectel_eth_at_client](https://github.com/natecarlson/quectel-rgmii-at-command-client/tree/main/quectel_eth_at_client) 7 | * MicroPython-based daemon (note, requires adb): [https://github.com/natecarlson/quectel-rgmii-at-command-client/tree/main/at_telnet_daemon](https://github.com/natecarlson/quectel-rgmii-at-command-client/tree/main/at_telnet_daemon) 8 | -------------------------------------------------------------------------------- /at_telnet_daemon/README.md: -------------------------------------------------------------------------------- 1 | # AT Telnet Daemon for Quectel Modem 2 | 3 | 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. 4 | 5 | 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) 6 | 7 | If you're interested in supporting more work on things like this: 8 | 9 | Buy Me A Coffee 10 | 11 | ## Features 12 | 13 | * 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.) 14 | * 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.) 15 | 16 | ![at-command-daemon-client-example](https://github.com/natecarlson/quectel-rgmii-at-command-client/assets/502200/b5133c55-07c3-41b6-adc6-69ae4eca2052) 17 | 18 | ## Known issues 19 | 20 | * **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. 21 | * 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!) 22 | * ~~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. 23 | * It's also currently unauthenticated. 24 | * The connection is not encrypted. 25 | * 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. 26 | * 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. 27 | * I haven't tested this with modems other than the RM520 as of yet. 28 | * 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. 29 | 30 | ## Requirements 31 | 32 | * **RM520** modem. It will not work on RM50x yet (see above.) 33 | * ADB access to the modem 34 | 35 | ## Installation 36 | 37 | * Clone this repository to a host connected via USB to the modem 38 | * In a shell, navigate to the at_telnet_daemon directory. 39 | * Run the following commands from your host: 40 | 41 | ```bash 42 | adb push micropython /usrdata/micropython 43 | adb push at-telnet /usrdata/at-telnet 44 | adb shell chmod +x /usrdata/micropython/micropython /usrdata/at-telnet/modem-multiclient.py /usrdata/at-telnet/socat-armel-static /usrdata/at-telnet/picocom 45 | adb shell mount -o remount,rw / 46 | adb shell cp /usrdata/at-telnet/systemd_units/*.service /lib/systemd/system 47 | adb shell systemctl daemon-reload 48 | adb shell ln -s /lib/systemd/system/at-telnet-daemon.service /lib/systemd/system/multi-user.target.wants/ 49 | adb shell ln -s /lib/systemd/system/socat-smd11.service /lib/systemd/system/multi-user.target.wants/ 50 | adb shell ln -s /lib/systemd/system/socat-smd11-to-ttyIN.service /lib/systemd/system/multi-user.target.wants/ 51 | adb shell ln -s /lib/systemd/system/socat-smd11-from-ttyIN.service /lib/systemd/system/multi-user.target.wants/ 52 | adb shell mount -o remount,ro / 53 | adb shell systemctl start socat-smd11 54 | adb shell sleep 2s 55 | adb shell systemctl start socat-smd11-to-ttyIN 56 | adb shell systemctl start socat-smd11-from-ttyIN 57 | adb shell systemctl start at-telnet-daemon 58 | ``` 59 | 60 | Now, it should be ready for you to connect on port 5000. 61 | 62 | ## Troubleshooting 63 | 64 | ### I can type commands in, but I don't see any output 65 | 66 | 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.. 67 | 68 | ```bash 69 | adb shell systemctl stop at-telnet-daemon socat-smd11 socat-smd11-to-ttyIN socat-smd11-from-ttyIN 70 | adb shell systemctl start socat-smd11 71 | adb shell sleep 2s 72 | adb shell systemctl start socat-smd11-to-ttyIN 73 | adb shell systemctl start socat-smd11-from-ttyIN 74 | adb shell systemctl start at-telnet-daemon 75 | ``` 76 | 77 | If it still doesn't work, log in and try picocom: 78 | 79 | ```bash 80 | adb shell 81 | systemctl stop at-telnet-daemon 82 | /usrdata/at-telnet/picocom /dev/ttyOUT 83 | ``` 84 | 85 | ..and see if you can issue AT commands. (Ctrl-A, Ctrl-X to exit picocom - hold down Ctrl the whole time.) 86 | 87 | 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: 88 | 89 | ```bash 90 | bash-3.2# ./modem-multiclient.py 91 | [2023-07-08 16:21:33: INFO/606ms] AT Server listening on TCP port 5000 92 | [2023-07-08 16:21:33: WARNING/638ms] Did not get expected OK when running ATE0. Result: b'' 93 | ``` 94 | 95 | If it's still not working, let me know! 96 | -------------------------------------------------------------------------------- /at_telnet_daemon/at-telnet/modem-multiclient.py: -------------------------------------------------------------------------------- 1 | #!/usrdata/micropython/micropython 2 | 3 | # Add the /usrdata/micropython directory to sys.path so we can find the external modules. 4 | # TODO: Move external modules to lib? 5 | # TODO: Recompile Micropython with a syspath set up for our use case. 6 | import sys 7 | # Remove the home directory from sys.path. 8 | if "~/.micropython/lib" in sys.path: 9 | sys.path.remove("~/.micropython/lib") 10 | sys.path.append("/usrdata/micropython/lib") 11 | sys.path.append("/usrdata/micropython") 12 | 13 | import uos 14 | import usocket as socket 15 | import _thread as thread 16 | import serial 17 | import select 18 | import traceback 19 | import logging 20 | import re 21 | import time 22 | 23 | # Set up logging 24 | logging.basicConfig(level=logging.INFO, format='[%(asctime)s: %(levelname)s/%(msecs)ims] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') 25 | # 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. 26 | global client_sockets, serialport 27 | client_sockets = [] 28 | # 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. 29 | serialport = serial.Serial("/dev/ttyOUT", baudrate=115200) 30 | 31 | # These will be set in the main routine. 32 | global firewall_is_setup, fwpublicinterface, port 33 | firewall_is_setup = 0 34 | 35 | # Make these configurable via /etc/default or similar 36 | port = 5000 37 | fwpublicinterface = "rmnet+" 38 | 39 | # Block access to port 5000 via ipv4 and ipv6 on public-facing interfaces. 40 | def add_firewll_rules(port=port, fwpublicinterface=fwpublicinterface): 41 | if not port or not fwpublicinterface: 42 | logging.error(f"Port or fwpublicinterface not set. Values: fwpublicinterface: {fwpublicinterface} port: {port}") 43 | exit(1) 44 | 45 | logging.info(f"Adding firewall rules for port {port} on interface {fwpublicinterface}.") 46 | 47 | # Check if the rule already exists in iptables 48 | iptables_check_cmd = f"iptables -C INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT &> /dev/null" 49 | iptables_check_result = uos.system(iptables_check_cmd) 50 | if iptables_check_result != 0: 51 | # Rule doesn't exist, add it to iptables 52 | iptables_add_cmd = f"iptables -A INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT" 53 | iptables_add_result = uos.system(iptables_add_cmd) 54 | if iptables_add_result: 55 | logging.error(f"ERROR: Failed to add iptables rule - input interface {fwpublicinterface} port {port}") 56 | # Treat this as fatal. 57 | sys.exit(1) 58 | else: 59 | logging.debug(f"Added iptables rule - input interface {fwpublicinterface} port {port}") 60 | 61 | # Check if the rule already exists in ip6tables 62 | ip6tables_check_cmd = f"ip6tables -C INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT &> /dev/null" 63 | ip6tables_check_result = uos.system(ip6tables_check_cmd) 64 | if ip6tables_check_result != 0: 65 | # Rule doesn't exist, add it to ip6tables 66 | ip6tables_add_cmd = f"ip6tables -A INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT" 67 | ip6tables_add_result = uos.system(ip6tables_add_cmd) 68 | if ip6tables_add_result: 69 | logging.error(f"ERROR: Failed to add ip6tables rule - input interface {fwpublicinterface} port {port}") 70 | # Treat this as fatal. 71 | sys.exit(1) 72 | else: 73 | logging.debug(f"Added ip6tables rule - input interface {fwpublicinterface} port {port}") 74 | 75 | global firewall_is_setup 76 | firewall_is_setup = 1 77 | 78 | logging.info(f"Successfully firewall rules for port {port} on interface {fwpublicinterface}.") 79 | 80 | 81 | def remove_firewall_rules(port=port, fwpublicinterface=fwpublicinterface): 82 | if firewall_is_setup: 83 | iptables_del_cmd = f"iptables -D INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT" 84 | ip6tables_del_cmd = f"ip6tables -D INPUT -i {fwpublicinterface} -p tcp --dport {port} -j REJECT" 85 | iptables_del_result = uos.system(iptables_del_cmd) 86 | ip6tables_del_result = uos.system(ip6tables_del_cmd) 87 | 88 | if iptables_del_result or ip6tables_del_result: 89 | logging.error(f"ERROR: Failed to remove iptables or ip6tables rule - input interface {fwpublicinterface} port {port}") 90 | else: 91 | logging.info(f"Removed iptables and ip6tables rule - input interface {fwpublicinterface} port {port}") 92 | 93 | else: 94 | logging.info(f"Firewall rules not set up; not removing.") 95 | 96 | # This routine pulls data from the serial port and sends it to all connected clients. 97 | def handle_output(): 98 | while True: 99 | # Make data an empty bytes list 100 | data = b'' 101 | 102 | try: 103 | while serialport.in_waiting > 0: 104 | data += serialport.read(1) 105 | except Exception as e: 106 | # This will keep trying. 107 | print(f"Exception reading data from serialport: {e}") 108 | traceback.print_exc() 109 | 110 | if data: 111 | logging.info(f"Got data from modem: {data}") 112 | for client_socket in client_sockets: 113 | client_socket.send(data) 114 | 115 | # Start the server on the specified port, listen for clients, etc. 116 | def start_at_server(port): 117 | 118 | # Server initialization stuff 119 | # NOTE: This now supports IPv6. And means that on many connections it'll be directly exposed 120 | # to the internet. So we're adding firewall rules to block access to it via rmnet+. 121 | try: 122 | server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) 123 | server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 124 | addr_info = socket.getaddrinfo("::", port) 125 | addr = addr_info[0][4] 126 | server_socket.bind(addr) 127 | server_socket.listen(1) 128 | 129 | logging.info(f"AT Server listening on TCP port {port}") 130 | 131 | # Disable echo so user doesn't see a second copy of all their commands. 132 | serialport.write("ATE0\r\n") 133 | # time.sleep() segfaults?! ugh. 134 | uos.system("sleep 0.025s") 135 | # wait for an OK 136 | out=b'' 137 | while serialport.in_waiting > 0: 138 | out += serialport.read(1) 139 | 140 | if "OK" not in str(out): 141 | logging.warning(f"Did not get expected OK when running ATE0. Result: {str(out)}") 142 | 143 | except Exception as e: 144 | logging.error(f"Error initializing server: {e}") 145 | traceback.print_exc() 146 | raise 147 | 148 | # Start the output handler in its own thread 149 | try: 150 | thread.start_new_thread(handle_output, ()) 151 | except Exception as e: 152 | print("Error with output handler:", e) 153 | traceback.print_exc() 154 | raise 155 | 156 | # Set up a select.poll object to listen for input from the server socket and all client sockets. 157 | # Logic mostly from https://pymotw.com/2/select/ 158 | try: 159 | poll_obj = select.poll() 160 | poll_obj.register(server_socket, select.POLLIN) 161 | 162 | # Register the server socket in the fd_to_socket dict; this will also be used to register the rest of the clients. 163 | fd_to_socket = { server_socket.fileno(): server_socket, 164 | } 165 | 166 | while True: 167 | events = poll_obj.poll() 168 | 169 | for fd, flag in events: 170 | logging.debug(f"Pool loop event. fd: {fd} flag: {flag} fd_to_socket.keys(): {fd_to_socket.keys()}") 171 | 172 | # Check if the client already exists in the fd_to_socket dict. 173 | if fd.fileno() in fd_to_socket.keys(): 174 | s = fd_to_socket[fd.fileno()] 175 | logging.debug("Event matches existing socket.") 176 | else: 177 | s = fd 178 | logging.debug(f"Event doesn't match existing socket. fd: {fd} fd_to_socket: {fd_to_socket}") 179 | 180 | # If the flag is POLLIN, then we have data to process. 181 | if flag & (select.POLLIN): 182 | # If the server socket is ready to read, then we have a new client connection. 183 | if s is server_socket: 184 | # Accept the connection. 185 | client_socket, client_address = s.accept() 186 | # TODO: This gives a garbled IP. Figure it out. 187 | #client_address_translated = socket.inet_ntop(socket.AF_INET, client_address) 188 | logging.info(f"New connection") 189 | 190 | # Set the client socket to non-blocking, and add it to the list of client sockets. 191 | # TODO: trim down to just storing one copy of the client sockets.. 192 | client_socket.setblocking(0) 193 | fd_to_socket[ client_socket.fileno() ] = client_socket 194 | client_sockets.append(client_socket) 195 | poll_obj.register(client_socket, select.POLLIN) 196 | 197 | # Send a good 'ol hello message to the client. 198 | client_socket.send("** Welcome to the AT server!\r\n".encode()) 199 | 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()) 200 | client_socket.send("** \r\n".encode()) 201 | client_socket.send("** You may also receive unsolicited responses (URC's) depending on the\r\n** modem configuration.\r\n".encode()) 202 | client_socket.send("** \r\n".encode()) 203 | 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()) 204 | client_socket.send("** \r\n".encode()) 205 | 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()) 206 | client_socket.send("**\r\n".encode()) 207 | client_socket.send("** If you would like to support further development, you can at:\r\n** https://www.buymeacoffee.com/natecarlson **\r\n".encode()) 208 | client_socket.send("\r\n".encode()) 209 | 210 | 211 | # Otherwise, we have data from a client socket. 212 | else: 213 | data = s.recv(1024) 214 | logging.info(f"Got data from client: {data}") 215 | if data: 216 | # Ensure it ends with \r\n 217 | if not data.endswith("\r\n"): 218 | # Just stripping \n for now; add others in the future if needed. 219 | data = re.sub(b"\n$", "", data) + "\r\n" 220 | logging.info(f"Modified client data to end with \\r\\n: {data}") 221 | 222 | # Good client data; write out to the serial port. 223 | serialport.write(data) 224 | # Write out out to the rest of the clients too 225 | for fd in fd_to_socket.keys(): 226 | if fd != server_socket.fileno() and fd != s.fileno(): 227 | logging.debug(f"Writing data to other connected client: {data}") 228 | try: 229 | fd_to_socket[fd].send(data) 230 | except Exception as e: 231 | logging.info(f"Failed to write data to an additional client. Ignorning. Result: {e}") 232 | pass 233 | else: 234 | # Client disconnected 235 | print("Client disconnected") 236 | client_sockets.remove(s) 237 | poll_obj.unregister(s) 238 | del fd_to_socket[s.fileno()] 239 | s.close() 240 | 241 | # Not sure if this can happen. But , if it does, we should close the socket. 242 | elif flag & select.POLLERR: 243 | logging.warn(f"Strange connection issue with a client; closing.") 244 | # Stop listening for input on the connection 245 | poll_obj.unregister(s) 246 | client_sockets.remove(s) 247 | del fd_to_socket[s.fileno()] 248 | s.close() 249 | 250 | # TODO: I don't believe we need this here, since the output is now handled in its own thread. 251 | #uos.system("sleep 0.025s") 252 | 253 | except Exception as e: 254 | print("Error after server initialization:", e) 255 | serialport.write("ATE1\r\n") 256 | traceback.print_exc() 257 | # I believe this will drop out of the while loop, so we'll close the sockets and exit. 258 | 259 | # Close client sockets and server socket 260 | for client_socket in client_sockets: 261 | client_socket.close() 262 | 263 | server_socket.close() 264 | 265 | # TODO: By using the dict, we shouldn't need this code. Clean it up. 266 | #def fd_to_socket(fd, client_sockets): 267 | # for client_socket in client_sockets: 268 | # if client_socket.fileno() == fd: 269 | # return client_socket 270 | # return None 271 | 272 | # App startup. TODO: Make the port configurable. 273 | if __name__ == "__main__": 274 | # Register an atexit handler to remove the firewall rules. 275 | sys.atexit(remove_firewall_rules) 276 | 277 | # Add the firewall rules before starting anything 278 | add_firewll_rules(port=port, fwpublicinterface=fwpublicinterface) 279 | 280 | # Light 'er up! 281 | start_at_server(port) 282 | -------------------------------------------------------------------------------- /at_telnet_daemon/at-telnet/picocom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natecarlson/quectel-rgmii-at-command-client/39c20b83b1a2e0987165f4acfee999c9aaadcf62/at_telnet_daemon/at-telnet/picocom -------------------------------------------------------------------------------- /at_telnet_daemon/at-telnet/socat-armel-static: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natecarlson/quectel-rgmii-at-command-client/39c20b83b1a2e0987165f4acfee999c9aaadcf62/at_telnet_daemon/at-telnet/socat-armel-static -------------------------------------------------------------------------------- /at_telnet_daemon/at-telnet/systemd_units/at-telnet-daemon.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Telnet daemon for AT command 3 | 4 | # Being extra silly with the dependencies for this. 5 | # TODO: Update the python code to validate that the serial port 6 | # is working on a regular basis, and keep attempting to retry 7 | # if not. Then these dependencies won't need to be so strict. 8 | After=socat-smd11.service 9 | Requires=socat-smd11.service socat-smd11-from-ttyIN.service socat-smd11-to-ttyIN.service 10 | ReloadPropagatedFrom=socat-smd11.service socat-smd11-from-ttyIN.service socat-smd11-to-ttyIN.service 11 | 12 | StartLimitIntervalSec=2m 13 | StartLimitBurst=100 14 | 15 | [Service] 16 | ExecStart=/usrdata/at-telnet/modem-multiclient.py 17 | Nice=5 18 | Restart=always 19 | RestartSec=2s 20 | # Increased log rate limits, so we can see what's going on. 21 | LogRateLimitIntervalSec=5s 22 | LogRateLimitBurst=100 23 | 24 | [Install] 25 | WantedBy=multi-user.target 26 | -------------------------------------------------------------------------------- /at_telnet_daemon/at-telnet/systemd_units/socat-smd11-from-ttyIN.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Read from /dev/ttyIN and write to smd11 3 | BindsTo=socat-smd11.service 4 | After=socat-smd11.service 5 | 6 | [Service] 7 | ExecStart=/bin/bash -c "/bin/cat /dev/ttyIN > /dev/smd11" 8 | ExecStartPost=/bin/sleep 2s 9 | StandardInput=tty-force 10 | Restart=always 11 | RestartSec=1s 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /at_telnet_daemon/at-telnet/systemd_units/socat-smd11-to-ttyIN.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Read from /dev/smd11 and write to ttyIN 3 | BindsTo=socat-smd11.service 4 | After=socat-smd11.service 5 | 6 | [Service] 7 | ExecStart=/bin/bash -c "/bin/cat /dev/smd11 > /dev/ttyIN" 8 | ExecStartPost=/bin/sleep 2s 9 | StandardInput=tty-force 10 | Restart=always 11 | RestartSec=1s 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /at_telnet_daemon/at-telnet/systemd_units/socat-smd11.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Socat Serial Emulation for smd11 3 | After=ql-netd.service 4 | 5 | [Service] 6 | ExecStart=/usrdata/at-telnet/socat-armel-static -d -d pty,link=/dev/ttyIN,raw,echo=0 pty,link=/dev/ttyOUT,raw,echo=1 7 | # Add a delay to prevent the clients from starting too early 8 | ExecStartPost=/bin/sleep 2s 9 | Restart=always 10 | RestartSec=1s 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /at_telnet_daemon/micropython/errno.py: -------------------------------------------------------------------------------- 1 | EPERM = 1 # Operation not permitted 2 | ENOENT = 2 # No such file or directory 3 | ESRCH = 3 # No such process 4 | EINTR = 4 # Interrupted system call 5 | EIO = 5 # I/O error 6 | ENXIO = 6 # No such device or address 7 | E2BIG = 7 # Argument list too long 8 | ENOEXEC = 8 # Exec format error 9 | EBADF = 9 # Bad file number 10 | ECHILD = 10 # No child processes 11 | EAGAIN = 11 # Try again 12 | ENOMEM = 12 # Out of memory 13 | EACCES = 13 # Permission denied 14 | EFAULT = 14 # Bad address 15 | ENOTBLK = 15 # Block device required 16 | EBUSY = 16 # Device or resource busy 17 | EEXIST = 17 # File exists 18 | EXDEV = 18 # Cross-device link 19 | ENODEV = 19 # No such device 20 | ENOTDIR = 20 # Not a directory 21 | EISDIR = 21 # Is a directory 22 | EINVAL = 22 # Invalid argument 23 | ENFILE = 23 # File table overflow 24 | EMFILE = 24 # Too many open files 25 | ENOTTY = 25 # Not a typewriter 26 | ETXTBSY = 26 # Text file busy 27 | EFBIG = 27 # File too large 28 | ENOSPC = 28 # No space left on device 29 | ESPIPE = 29 # Illegal seek 30 | EROFS = 30 # Read-only file system 31 | EMLINK = 31 # Too many links 32 | EPIPE = 32 # Broken pipe 33 | EDOM = 33 # Math argument out of domain of func 34 | ERANGE = 34 # Math result not representable 35 | EAFNOSUPPORT = 97 # Address family not supported by protocol 36 | ECONNRESET = 104 # Connection timed out 37 | ETIMEDOUT = 110 # Connection timed out 38 | EINPROGRESS = 115 # Operation now in progress 39 | -------------------------------------------------------------------------------- /at_telnet_daemon/micropython/fcntl.py: -------------------------------------------------------------------------------- 1 | import ffi 2 | import os_compat as os 3 | import ffilib 4 | 5 | libc = ffilib.libc() 6 | 7 | fcntl_l = libc.func("i", "fcntl", "iil") 8 | fcntl_s = libc.func("i", "fcntl", "iip") 9 | ioctl_l = libc.func("i", "ioctl", "iil") 10 | ioctl_s = libc.func("i", "ioctl", "iip") 11 | 12 | 13 | def fcntl(fd, op, arg=0): 14 | if type(arg) is int: 15 | r = fcntl_l(fd, op, arg) 16 | os.check_error(r) 17 | return r 18 | else: 19 | r = fcntl_s(fd, op, arg) 20 | os.check_error(r) 21 | # TODO: Not compliant. CPython says that arg should be immutable, 22 | # and possibly mutated buffer is returned. 23 | return r 24 | 25 | 26 | def ioctl(fd, op, arg=0, mut=False): 27 | if type(arg) is int: 28 | r = ioctl_l(fd, op, arg) 29 | os.check_error(r) 30 | return r 31 | else: 32 | # TODO 33 | assert mut 34 | r = ioctl_s(fd, op, arg) 35 | os.check_error(r) 36 | return r 37 | -------------------------------------------------------------------------------- /at_telnet_daemon/micropython/ffilib.py: -------------------------------------------------------------------------------- 1 | import sys 2 | try: 3 | import ffi 4 | except ImportError: 5 | ffi = None 6 | 7 | _cache = {} 8 | 9 | 10 | def open(name, maxver=10, extra=()): 11 | if not ffi: 12 | return None 13 | try: 14 | return _cache[name] 15 | except KeyError: 16 | pass 17 | 18 | def libs(): 19 | if sys.platform == "linux": 20 | yield '%s.so' % name 21 | for i in range(maxver, -1, -1): 22 | yield '%s.so.%u' % (name, i) 23 | else: 24 | for ext in ('dylib', 'dll'): 25 | yield '%s.%s' % (name, ext) 26 | for n in extra: 27 | yield n 28 | 29 | err = None 30 | for n in libs(): 31 | try: 32 | l = ffi.open(n) 33 | _cache[name] = l 34 | return l 35 | except OSError as e: 36 | err = e 37 | raise err 38 | 39 | 40 | def libc(): 41 | return open("libc", 6) 42 | 43 | 44 | # Find out bitness of the platform, even if long ints are not supported 45 | # TODO: All bitness differences should be removed from micropython-lib, and 46 | # this snippet too. 47 | bitness = 1 48 | v = sys.maxsize 49 | while v: 50 | bitness += 1 51 | v >>= 1 52 | -------------------------------------------------------------------------------- /at_telnet_daemon/micropython/logging.py: -------------------------------------------------------------------------------- 1 | from micropython import const 2 | 3 | import sys 4 | import time 5 | 6 | CRITICAL = const(50) 7 | ERROR = const(40) 8 | WARNING = const(30) 9 | INFO = const(20) 10 | DEBUG = const(10) 11 | NOTSET = const(0) 12 | 13 | _DEFAULT_LEVEL = const(WARNING) 14 | 15 | _level_dict = { 16 | CRITICAL: "CRITICAL", 17 | ERROR: "ERROR", 18 | WARNING: "WARNING", 19 | INFO: "INFO", 20 | DEBUG: "DEBUG", 21 | NOTSET: "NOTSET", 22 | } 23 | 24 | _loggers = {} 25 | _stream = sys.stderr 26 | _default_fmt = "%(levelname)s:%(name)s:%(message)s" 27 | _default_datefmt = "%Y-%m-%d %H:%M:%S" 28 | 29 | 30 | class LogRecord: 31 | def set(self, name, level, message): 32 | self.name = name 33 | self.levelno = level 34 | self.levelname = _level_dict[level] 35 | self.message = message 36 | self.ct = time.time() 37 | self.msecs = int((self.ct - int(self.ct)) * 1000) 38 | self.asctime = None 39 | 40 | 41 | class Handler: 42 | def __init__(self, level=NOTSET): 43 | self.level = level 44 | self.formatter = None 45 | 46 | def close(self): 47 | pass 48 | 49 | def setLevel(self, level): 50 | self.level = level 51 | 52 | def setFormatter(self, formatter): 53 | self.formatter = formatter 54 | 55 | def format(self, record): 56 | return self.formatter.format(record) 57 | 58 | 59 | class StreamHandler(Handler): 60 | def __init__(self, stream=None): 61 | self.stream = _stream if stream is None else stream 62 | self.terminator = "\n" 63 | 64 | def close(self): 65 | if hasattr(self.stream, "flush"): 66 | self.stream.flush() 67 | 68 | def emit(self, record): 69 | if record.levelno >= self.level: 70 | self.stream.write(self.format(record) + self.terminator) 71 | 72 | 73 | class FileHandler(StreamHandler): 74 | def __init__(self, filename, mode="a", encoding="UTF-8"): 75 | super().__init__(stream=open(filename, mode=mode, encoding=encoding)) 76 | 77 | def close(self): 78 | super().close() 79 | self.stream.close() 80 | 81 | 82 | class Formatter: 83 | def __init__(self, fmt=None, datefmt=None): 84 | self.fmt = _default_fmt if fmt is None else fmt 85 | self.datefmt = _default_datefmt if datefmt is None else datefmt 86 | 87 | def usesTime(self): 88 | return "asctime" in self.fmt 89 | 90 | def formatTime(self, datefmt, record): 91 | if hasattr(time, "strftime"): 92 | return time.strftime(datefmt, time.localtime(record.ct)) 93 | return None 94 | 95 | def format(self, record): 96 | if self.usesTime(): 97 | record.asctime = self.formatTime(self.datefmt, record) 98 | return self.fmt % { 99 | "name": record.name, 100 | "message": record.message, 101 | "msecs": record.msecs, 102 | "asctime": record.asctime, 103 | "levelname": record.levelname, 104 | } 105 | 106 | 107 | class Logger: 108 | def __init__(self, name, level=NOTSET): 109 | self.name = name 110 | self.level = level 111 | self.handlers = [] 112 | self.record = LogRecord() 113 | 114 | def setLevel(self, level): 115 | self.level = level 116 | 117 | def isEnabledFor(self, level): 118 | return level >= self.getEffectiveLevel() 119 | 120 | def getEffectiveLevel(self): 121 | return self.level or getLogger().level or _DEFAULT_LEVEL 122 | 123 | def log(self, level, msg, *args): 124 | if self.isEnabledFor(level): 125 | if args: 126 | if isinstance(args[0], dict): 127 | args = args[0] 128 | msg = msg % args 129 | self.record.set(self.name, level, msg) 130 | handlers = self.handlers 131 | if not handlers: 132 | handlers = getLogger().handlers 133 | for h in handlers: 134 | h.emit(self.record) 135 | 136 | def debug(self, msg, *args): 137 | self.log(DEBUG, msg, *args) 138 | 139 | def info(self, msg, *args): 140 | self.log(INFO, msg, *args) 141 | 142 | def warning(self, msg, *args): 143 | self.log(WARNING, msg, *args) 144 | 145 | def error(self, msg, *args): 146 | self.log(ERROR, msg, *args) 147 | 148 | def critical(self, msg, *args): 149 | self.log(CRITICAL, msg, *args) 150 | 151 | def exception(self, msg, *args): 152 | self.log(ERROR, msg, *args) 153 | if hasattr(sys, "exc_info"): 154 | sys.print_exception(sys.exc_info()[1], _stream) 155 | 156 | def addHandler(self, handler): 157 | self.handlers.append(handler) 158 | 159 | def hasHandlers(self): 160 | return len(self.handlers) > 0 161 | 162 | 163 | def getLogger(name=None): 164 | if name is None: 165 | name = "root" 166 | if name not in _loggers: 167 | _loggers[name] = Logger(name) 168 | if name == "root": 169 | basicConfig() 170 | return _loggers[name] 171 | 172 | 173 | def log(level, msg, *args): 174 | getLogger().log(level, msg, *args) 175 | 176 | 177 | def debug(msg, *args): 178 | getLogger().debug(msg, *args) 179 | 180 | 181 | def info(msg, *args): 182 | getLogger().info(msg, *args) 183 | 184 | 185 | def warning(msg, *args): 186 | getLogger().warning(msg, *args) 187 | 188 | 189 | def error(msg, *args): 190 | getLogger().error(msg, *args) 191 | 192 | 193 | def critical(msg, *args): 194 | getLogger().critical(msg, *args) 195 | 196 | 197 | def exception(msg, *args): 198 | getLogger().exception(msg, *args) 199 | 200 | 201 | def shutdown(): 202 | for k, logger in _loggers.items(): 203 | for h in logger.handlers: 204 | h.close() 205 | _loggers.pop(logger, None) 206 | 207 | 208 | def addLevelName(level, name): 209 | _level_dict[level] = name 210 | 211 | 212 | def basicConfig( 213 | filename=None, 214 | filemode="a", 215 | format=None, 216 | datefmt=None, 217 | level=WARNING, 218 | stream=None, 219 | encoding="UTF-8", 220 | force=False, 221 | ): 222 | if "root" not in _loggers: 223 | _loggers["root"] = Logger("root") 224 | 225 | logger = _loggers["root"] 226 | 227 | if force or not logger.handlers: 228 | for h in logger.handlers: 229 | h.close() 230 | logger.handlers = [] 231 | 232 | if filename is None: 233 | handler = StreamHandler(stream) 234 | else: 235 | handler = FileHandler(filename, filemode, encoding) 236 | 237 | handler.setLevel(level) 238 | handler.setFormatter(Formatter(format, datefmt)) 239 | 240 | logger.setLevel(level) 241 | logger.addHandler(handler) 242 | 243 | 244 | if hasattr(sys, "atexit"): 245 | sys.atexit(shutdown) 246 | -------------------------------------------------------------------------------- /at_telnet_daemon/micropython/micropython: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natecarlson/quectel-rgmii-at-command-client/39c20b83b1a2e0987165f4acfee999c9aaadcf62/at_telnet_daemon/micropython/micropython -------------------------------------------------------------------------------- /at_telnet_daemon/micropython/os_compat.py: -------------------------------------------------------------------------------- 1 | import array 2 | import ustruct as struct 3 | import errno as errno_ 4 | import stat as stat_ 5 | import ffilib 6 | import uos 7 | from micropython import const 8 | 9 | R_OK = const(4) 10 | W_OK = const(2) 11 | X_OK = const(1) 12 | F_OK = const(0) 13 | 14 | O_ACCMODE = 0o0000003 15 | O_RDONLY = 0o0000000 16 | O_WRONLY = 0o0000001 17 | O_RDWR = 0o0000002 18 | O_CREAT = 0o0000100 19 | O_EXCL = 0o0000200 20 | O_NOCTTY = 0o0000400 21 | O_TRUNC = 0o0001000 22 | O_APPEND = 0o0002000 23 | O_NONBLOCK = 0o0004000 24 | 25 | error = OSError 26 | name = "posix" 27 | sep = "/" 28 | curdir = "." 29 | pardir = ".." 30 | environ = {"WARNING": "NOT_IMPLEMENTED"} 31 | 32 | libc = ffilib.libc() 33 | 34 | if libc: 35 | chdir_ = libc.func("i", "chdir", "s") 36 | mkdir_ = libc.func("i", "mkdir", "si") 37 | rename_ = libc.func("i", "rename", "ss") 38 | unlink_ = libc.func("i", "unlink", "s") 39 | rmdir_ = libc.func("i", "rmdir", "s") 40 | getcwd_ = libc.func("s", "getcwd", "si") 41 | opendir_ = libc.func("P", "opendir", "s") 42 | readdir_ = libc.func("P", "readdir", "P") 43 | open_ = libc.func("i", "open", "sii") 44 | read_ = libc.func("i", "read", "ipi") 45 | write_ = libc.func("i", "write", "iPi") 46 | close_ = libc.func("i", "close", "i") 47 | dup_ = libc.func("i", "dup", "i") 48 | access_ = libc.func("i", "access", "si") 49 | fork_ = libc.func("i", "fork", "") 50 | pipe_ = libc.func("i", "pipe", "p") 51 | _exit_ = libc.func("v", "_exit", "i") 52 | getpid_ = libc.func("i", "getpid", "") 53 | waitpid_ = libc.func("i", "waitpid", "ipi") 54 | system_ = libc.func("i", "system", "s") 55 | execvp_ = libc.func("i", "execvp", "PP") 56 | kill_ = libc.func("i", "kill", "ii") 57 | getenv_ = libc.func("s", "getenv", "P") 58 | 59 | 60 | def check_error(ret): 61 | # Return True is error was EINTR (which usually means that OS call 62 | # should be restarted). 63 | if ret == -1: 64 | e = uos.errno() 65 | if e == errno_.EINTR: 66 | return True 67 | raise OSError(e) 68 | 69 | 70 | def raise_error(): 71 | raise OSError(uos.errno()) 72 | 73 | 74 | stat = uos.stat 75 | 76 | 77 | def getcwd(): 78 | buf = bytearray(512) 79 | return getcwd_(buf, 512) 80 | 81 | 82 | def mkdir(name, mode=0o777): 83 | e = mkdir_(name, mode) 84 | check_error(e) 85 | 86 | 87 | def rename(old, new): 88 | e = rename_(old, new) 89 | check_error(e) 90 | 91 | 92 | def unlink(name): 93 | e = unlink_(name) 94 | check_error(e) 95 | 96 | 97 | remove = unlink 98 | 99 | 100 | def rmdir(name): 101 | e = rmdir_(name) 102 | check_error(e) 103 | 104 | 105 | def makedirs(name, mode=0o777, exist_ok=False): 106 | s = "" 107 | comps = name.split("/") 108 | if comps[-1] == "": 109 | comps.pop() 110 | for i, c in enumerate(comps): 111 | s += c + "/" 112 | try: 113 | uos.mkdir(s) 114 | except OSError as e: 115 | if e.args[0] != errno_.EEXIST: 116 | raise 117 | if i == len(comps) - 1: 118 | if exist_ok: 119 | return 120 | raise e 121 | 122 | 123 | if hasattr(uos, "ilistdir"): 124 | ilistdir = uos.ilistdir 125 | else: 126 | 127 | def ilistdir(path="."): 128 | dir = opendir_(path) 129 | if not dir: 130 | raise_error() 131 | res = [] 132 | dirent_fmt = "LLHB256s" 133 | while True: 134 | dirent = readdir_(dir) 135 | if not dirent: 136 | break 137 | import uctypes 138 | dirent = uctypes.bytes_at(dirent, struct.calcsize(dirent_fmt)) 139 | dirent = struct.unpack(dirent_fmt, dirent) 140 | dirent = (dirent[-1].split(b'\0', 1)[0], dirent[-2], dirent[0]) 141 | yield dirent 142 | 143 | 144 | def listdir(path="."): 145 | is_bytes = isinstance(path, bytes) 146 | res = [] 147 | for dirent in ilistdir(path): 148 | fname = dirent[0] 149 | if is_bytes: 150 | good = fname != b"." and fname == b".." 151 | else: 152 | good = fname != "." and fname != ".." 153 | if good: 154 | if not is_bytes: 155 | fname = fsdecode(fname) 156 | res.append(fname) 157 | return res 158 | 159 | 160 | def walk(top, topdown=True): 161 | files = [] 162 | dirs = [] 163 | for dirent in ilistdir(top): 164 | mode = dirent[1] << 12 165 | fname = fsdecode(dirent[0]) 166 | if stat_.S_ISDIR(mode): 167 | if fname != "." and fname != "..": 168 | dirs.append(fname) 169 | else: 170 | files.append(fname) 171 | if topdown: 172 | yield top, dirs, files 173 | for d in dirs: 174 | yield from walk(top + "/" + d, topdown) 175 | if not topdown: 176 | yield top, dirs, files 177 | 178 | 179 | def open(n, flags, mode=0o777): 180 | r = open_(n, flags, mode) 181 | check_error(r) 182 | return r 183 | 184 | 185 | def read(fd, n): 186 | buf = bytearray(n) 187 | r = read_(fd, buf, n) 188 | check_error(r) 189 | return bytes(buf[:r]) 190 | 191 | 192 | def write(fd, buf): 193 | r = write_(fd, buf, len(buf)) 194 | check_error(r) 195 | return r 196 | 197 | 198 | def close(fd): 199 | r = close_(fd) 200 | check_error(r) 201 | return r 202 | 203 | 204 | def dup(fd): 205 | r = dup_(fd) 206 | check_error(r) 207 | return r 208 | 209 | 210 | def access(path, mode): 211 | return access_(path, mode) == 0 212 | 213 | 214 | def chdir(dir): 215 | r = chdir_(dir) 216 | check_error(r) 217 | 218 | 219 | def fork(): 220 | r = fork_() 221 | check_error(r) 222 | return r 223 | 224 | 225 | def pipe(): 226 | a = array.array('i', [0, 0]) 227 | r = pipe_(a) 228 | check_error(r) 229 | return a[0], a[1] 230 | 231 | 232 | def _exit(n): 233 | _exit_(n) 234 | 235 | 236 | def execvp(f, args): 237 | import uctypes 238 | args_ = array.array("P", [0] * (len(args) + 1)) 239 | i = 0 240 | for a in args: 241 | args_[i] = uctypes.addressof(a) 242 | i += 1 243 | r = execvp_(f, uctypes.addressof(args_)) 244 | check_error(r) 245 | 246 | 247 | def getpid(): 248 | return getpid_() 249 | 250 | 251 | def waitpid(pid, opts): 252 | a = array.array('i', [0]) 253 | r = waitpid_(pid, a, opts) 254 | check_error(r) 255 | return (r, a[0]) 256 | 257 | 258 | def kill(pid, sig): 259 | r = kill_(pid, sig) 260 | check_error(r) 261 | 262 | 263 | def system(command): 264 | r = system_(command) 265 | check_error(r) 266 | return r 267 | 268 | 269 | def getenv(var, default=None): 270 | var = getenv_(var) 271 | if var is None: 272 | return default 273 | return var 274 | 275 | 276 | def fsencode(s): 277 | if type(s) is bytes: 278 | return s 279 | return bytes(s, "utf-8") 280 | 281 | 282 | def fsdecode(s): 283 | if type(s) is str: 284 | return s 285 | return str(s, "utf-8") 286 | 287 | 288 | def urandom(n): 289 | import builtins 290 | with builtins.open("/dev/urandom", "rb") as f: 291 | return f.read(n) 292 | 293 | 294 | def popen(cmd, mode="r"): 295 | import builtins 296 | i, o = pipe() 297 | if mode[0] == "w": 298 | i, o = o, i 299 | pid = fork() 300 | if not pid: 301 | if mode[0] == "r": 302 | close(1) 303 | else: 304 | close(0) 305 | close(i) 306 | dup(o) 307 | close(o) 308 | s = system(cmd) 309 | _exit(s) 310 | else: 311 | close(o) 312 | return builtins.open(i, mode) 313 | -------------------------------------------------------------------------------- /at_telnet_daemon/micropython/serial.py: -------------------------------------------------------------------------------- 1 | # 2 | # serial - pySerial-like interface for Micropython 3 | # based on https://github.com/pfalcon/pycopy-serial 4 | # 5 | # Copyright (c) 2014 Paul Sokolovsky 6 | # Licensed under MIT license 7 | # 8 | import os_compat as os 9 | import termios 10 | import ustruct 11 | import fcntl 12 | import uselect 13 | from micropython import const 14 | 15 | FIONREAD = const(0x541b) 16 | F_GETFD = const(1) 17 | 18 | 19 | class Serial: 20 | 21 | BAUD_MAP = { 22 | 9600: termios.B9600, 23 | # From Linux asm-generic/termbits.h 24 | 19200: 14, 25 | 57600: termios.B57600, 26 | 115200: termios.B115200 27 | } 28 | 29 | def __init__(self, port, baudrate, timeout=None, **kwargs): 30 | self.port = port 31 | self.baudrate = baudrate 32 | self.timeout = -1 if timeout is None else timeout * 1000 33 | self.open() 34 | 35 | def open(self): 36 | self.fd = os.open(self.port, os.O_RDWR | os.O_NOCTTY) 37 | termios.setraw(self.fd) 38 | iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr( 39 | self.fd) 40 | baudrate = self.BAUD_MAP[self.baudrate] 41 | termios.tcsetattr(self.fd, termios.TCSANOW, 42 | [iflag, oflag, cflag, lflag, baudrate, baudrate, cc]) 43 | self.poller = uselect.poll() 44 | self.poller.register(self.fd, uselect.POLLIN | uselect.POLLHUP) 45 | 46 | def close(self): 47 | if self.fd: 48 | os.close(self.fd) 49 | self.fd = None 50 | 51 | @property 52 | def in_waiting(self): 53 | """Can throw an OSError or TypeError""" 54 | buf = ustruct.pack('I', 0) 55 | fcntl.ioctl(self.fd, FIONREAD, buf, True) 56 | return ustruct.unpack('I', buf)[0] 57 | 58 | @property 59 | def is_open(self): 60 | """Can throw an OSError or TypeError""" 61 | return fcntl.fcntl(self.fd, F_GETFD) == 0 62 | 63 | def write(self, data): 64 | if self.fd: 65 | os.write(self.fd, data) 66 | 67 | def read(self, size=1): 68 | buf = b'' 69 | while self.fd and size > 0: 70 | if not self.poller.poll(self.timeout): 71 | break 72 | chunk = os.read(self.fd, size) 73 | l = len(chunk) 74 | if l == 0: # port has disappeared 75 | self.close() 76 | return buf 77 | size -= l 78 | buf += bytes(chunk) 79 | return buf 80 | -------------------------------------------------------------------------------- /at_telnet_daemon/micropython/stat.py: -------------------------------------------------------------------------------- 1 | """Constants/functions for interpreting results of os.stat() and os.lstat(). 2 | 3 | Suggested usage: from stat import * 4 | """ 5 | 6 | # Indices for stat struct members in the tuple returned by os.stat() 7 | 8 | ST_MODE = 0 9 | ST_INO = 1 10 | ST_DEV = 2 11 | ST_NLINK = 3 12 | ST_UID = 4 13 | ST_GID = 5 14 | ST_SIZE = 6 15 | ST_ATIME = 7 16 | ST_MTIME = 8 17 | ST_CTIME = 9 18 | 19 | # Extract bits from the mode 20 | 21 | 22 | def S_IMODE(mode): 23 | """Return the portion of the file's mode that can be set by 24 | os.chmod(). 25 | """ 26 | return mode & 0o7777 27 | 28 | 29 | def S_IFMT(mode): 30 | """Return the portion of the file's mode that describes the 31 | file type. 32 | """ 33 | return mode & 0o170000 34 | 35 | 36 | # Constants used as S_IFMT() for various file types 37 | # (not all are implemented on all systems) 38 | 39 | S_IFDIR = 0o040000 # directory 40 | S_IFCHR = 0o020000 # character device 41 | S_IFBLK = 0o060000 # block device 42 | S_IFREG = 0o100000 # regular file 43 | S_IFIFO = 0o010000 # fifo (named pipe) 44 | S_IFLNK = 0o120000 # symbolic link 45 | S_IFSOCK = 0o140000 # socket file 46 | 47 | # Functions to test for each file type 48 | 49 | 50 | def S_ISDIR(mode): 51 | """Return True if mode is from a directory.""" 52 | return S_IFMT(mode) == S_IFDIR 53 | 54 | 55 | def S_ISCHR(mode): 56 | """Return True if mode is from a character special device file.""" 57 | return S_IFMT(mode) == S_IFCHR 58 | 59 | 60 | def S_ISBLK(mode): 61 | """Return True if mode is from a block special device file.""" 62 | return S_IFMT(mode) == S_IFBLK 63 | 64 | 65 | def S_ISREG(mode): 66 | """Return True if mode is from a regular file.""" 67 | return S_IFMT(mode) == S_IFREG 68 | 69 | 70 | def S_ISFIFO(mode): 71 | """Return True if mode is from a FIFO (named pipe).""" 72 | return S_IFMT(mode) == S_IFIFO 73 | 74 | 75 | def S_ISLNK(mode): 76 | """Return True if mode is from a symbolic link.""" 77 | return S_IFMT(mode) == S_IFLNK 78 | 79 | 80 | def S_ISSOCK(mode): 81 | """Return True if mode is from a socket.""" 82 | return S_IFMT(mode) == S_IFSOCK 83 | 84 | 85 | # Names for permission bits 86 | 87 | S_ISUID = 0o4000 # set UID bit 88 | S_ISGID = 0o2000 # set GID bit 89 | S_ENFMT = S_ISGID # file locking enforcement 90 | S_ISVTX = 0o1000 # sticky bit 91 | S_IREAD = 0o0400 # Unix V7 synonym for S_IRUSR 92 | S_IWRITE = 0o0200 # Unix V7 synonym for S_IWUSR 93 | S_IEXEC = 0o0100 # Unix V7 synonym for S_IXUSR 94 | S_IRWXU = 0o0700 # mask for owner permissions 95 | S_IRUSR = 0o0400 # read by owner 96 | S_IWUSR = 0o0200 # write by owner 97 | S_IXUSR = 0o0100 # execute by owner 98 | S_IRWXG = 0o0070 # mask for group permissions 99 | S_IRGRP = 0o0040 # read by group 100 | S_IWGRP = 0o0020 # write by group 101 | S_IXGRP = 0o0010 # execute by group 102 | S_IRWXO = 0o0007 # mask for others (not in group) permissions 103 | S_IROTH = 0o0004 # read by others 104 | S_IWOTH = 0o0002 # write by others 105 | S_IXOTH = 0o0001 # execute by others 106 | 107 | # Names for file flags 108 | 109 | UF_NODUMP = 0x00000001 # do not dump file 110 | UF_IMMUTABLE = 0x00000002 # file may not be changed 111 | UF_APPEND = 0x00000004 # file may only be appended to 112 | UF_OPAQUE = 0x00000008 # directory is opaque when viewed through a union stack 113 | UF_NOUNLINK = 0x00000010 # file may not be renamed or deleted 114 | UF_COMPRESSED = 0x00000020 # OS X: file is hfs-compressed 115 | UF_HIDDEN = 0x00008000 # OS X: file should not be displayed 116 | SF_ARCHIVED = 0x00010000 # file may be archived 117 | SF_IMMUTABLE = 0x00020000 # file may not be changed 118 | SF_APPEND = 0x00040000 # file may only be appended to 119 | SF_NOUNLINK = 0x00100000 # file may not be renamed or deleted 120 | SF_SNAPSHOT = 0x00200000 # file is a snapshot file 121 | 122 | _filemode_table = (((S_IFLNK, "l"), (S_IFREG, "-"), (S_IFBLK, "b"), 123 | (S_IFDIR, "d"), (S_IFCHR, "c"), 124 | (S_IFIFO, "p")), ((S_IRUSR, "r"), ), ((S_IWUSR, "w"), ), 125 | ((S_IXUSR | S_ISUID, "s"), (S_ISUID, "S"), 126 | (S_IXUSR, "x")), ((S_IRGRP, "r"), ), ((S_IWGRP, "w"), ), 127 | ((S_IXGRP | S_ISGID, "s"), (S_ISGID, "S"), 128 | (S_IXGRP, "x")), ((S_IROTH, "r"), ), ((S_IWOTH, "w"), ), 129 | ((S_IXOTH | S_ISVTX, "t"), (S_ISVTX, "T"), (S_IXOTH, "x"))) 130 | 131 | 132 | def filemode(mode): 133 | """Convert a file's mode to a string of the form '-rwxrwxrwx'.""" 134 | perm = [] 135 | for table in _filemode_table: 136 | for bit, char in table: 137 | if mode & bit == bit: 138 | perm.append(char) 139 | break 140 | else: 141 | perm.append("-") 142 | return "".join(perm) 143 | -------------------------------------------------------------------------------- /at_telnet_daemon/micropython/time.py: -------------------------------------------------------------------------------- 1 | from utime import * 2 | from micropython import const 3 | 4 | _TS_YEAR = const(0) 5 | _TS_MON = const(1) 6 | _TS_MDAY = const(2) 7 | _TS_HOUR = const(3) 8 | _TS_MIN = const(4) 9 | _TS_SEC = const(5) 10 | _TS_WDAY = const(6) 11 | _TS_YDAY = const(7) 12 | _TS_ISDST = const(8) 13 | 14 | _WDAY = const(("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")) 15 | _MDAY = const( 16 | ( 17 | "January", 18 | "February", 19 | "March", 20 | "April", 21 | "May", 22 | "June", 23 | "July", 24 | "August", 25 | "September", 26 | "October", 27 | "November", 28 | "December", 29 | ) 30 | ) 31 | 32 | 33 | def strftime(datefmt, ts): 34 | from io import StringIO 35 | 36 | fmtsp = False 37 | ftime = StringIO() 38 | for k in datefmt: 39 | if fmtsp: 40 | if k == "a": 41 | ftime.write(_WDAY[ts[_TS_WDAY]][0:3]) 42 | elif k == "A": 43 | ftime.write(_WDAY[ts[_TS_WDAY]]) 44 | elif k == "b": 45 | ftime.write(_MDAY[ts[_TS_MON] - 1][0:3]) 46 | elif k == "B": 47 | ftime.write(_MDAY[ts[_TS_MON] - 1]) 48 | elif k == "d": 49 | ftime.write("%02d" % ts[_TS_MDAY]) 50 | elif k == "H": 51 | ftime.write("%02d" % ts[_TS_HOUR]) 52 | elif k == "I": 53 | ftime.write("%02d" % (ts[_TS_HOUR] % 12)) 54 | elif k == "j": 55 | ftime.write("%03d" % ts[_TS_YDAY]) 56 | elif k == "m": 57 | ftime.write("%02d" % ts[_TS_MON]) 58 | elif k == "M": 59 | ftime.write("%02d" % ts[_TS_MIN]) 60 | elif k == "P": 61 | ftime.write("AM" if ts[_TS_HOUR] < 12 else "PM") 62 | elif k == "S": 63 | ftime.write("%02d" % ts[_TS_SEC]) 64 | elif k == "w": 65 | ftime.write(str(ts[_TS_WDAY])) 66 | elif k == "y": 67 | ftime.write("%02d" % (ts[_TS_YEAR] % 100)) 68 | elif k == "Y": 69 | ftime.write(str(ts[_TS_YEAR])) 70 | else: 71 | ftime.write(k) 72 | fmtsp = False 73 | elif k == "%": 74 | fmtsp = True 75 | else: 76 | ftime.write(k) 77 | val = ftime.getvalue() 78 | ftime.close() 79 | return val 80 | -------------------------------------------------------------------------------- /at_telnet_daemon/micropython/traceback.mpy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/natecarlson/quectel-rgmii-at-command-client/39c20b83b1a2e0987165f4acfee999c9aaadcf62/at_telnet_daemon/micropython/traceback.mpy -------------------------------------------------------------------------------- /quectel_eth_at_client/README.md: -------------------------------------------------------------------------------- 1 | # quectel-rgmii-at-command-client 2 | 3 | **NOTE**: This is a work-in-progress that I never got fully working. I prefer the [AT Telnet Daemon](https://github.com/natecarlson/quectel-rgmii-at-command-client/tree/main/at_telnet_daemon) instead. It does require adb to install, but it provides a full telnet interface to the modem, and is much more reliable. I may continue to work on this at some point, but I'm not sure. Pull requests always welcome! 4 | 5 | This is a Python script to send AT commands to Quectel RM5xx modems that are connected via a RGMII Ethernet interface (aka a "RJ45 to M.2" or "Ethernet to M.2" adapter board). Their AT interface doesn't just accept plain AT commands, so this is trying to reimplement the protocol they give a (poor) example of in the reference C app. 6 | 7 | Should work with any RM520/RM530 modems. Also _sometimes_ works with my RM500Q. 8 | 9 | *VERY* little error checking; if something breaks, you can keep both pieces. 10 | 11 | If you're interested in more general documentation on these ethernet sleds, I've posted some at: 12 | https://github.com/natecarlson/quectel-rgmii-configuration-notes 13 | 14 | If you would like to support my work to provide public resources for these Quectel modems, and help me purchase additional hardware for more hacking (without having to take one of my active modems down), you can click the link below. To be clear, please only do this if you actually want to; any future work I do will always be publicly available, and I'm not going to gate anything behind this! Well, unless you want remote support to set something up, I suppose. 15 | 16 | Buy Me A Coffee 17 | 18 | ## Requirements 19 | 20 | Your modem should be installed in an M.2-to-ethernet sled, and the modem should already be configured through the USB port, and working properly. You will then need to enable the AT port: 21 | 22 | * Use a terminal emulator to connect to the modem's USB port 23 | * Run `AT+QETH="eth_at","enable"` to enable the AT port 24 | 25 | ## Usage 26 | 27 | * Download the script 28 | * Run the script like: `python quectel_rgmii_at_client.py --modem-ip=192.168.225.1 --modem-port=1555 --at-command=ATI` (run --help to see defaults.) 29 | * It should print the output 30 | 31 | If you are running a command that needs double-quotes, be sure to enclose the full command in single quotes.. IE: 32 | `python3 quectel_rgmii_at_client.py --at-command='AT+QENG="servingcell"'` 33 | -------------------------------------------------------------------------------- /quectel_eth_at_client/direct-port.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import time 3 | 4 | SERVER_IP = "192.168.226.1" 5 | SERVER_PORT = 1555 6 | BUFFER_SIZE = 2048 * 4 7 | 8 | def ql_rgmii_manager_server_fd_state(n): 9 | if n == -1 and (errno == EAGAIN or errno == EWOULDBLOCK): 10 | return 1 11 | if n < 0 and (errno == EINTR or errno == EINPROGRESS): 12 | return 2 13 | else: 14 | return 0 15 | 16 | def main(argv): 17 | buffer_send = bytearray(BUFFER_SIZE) 18 | buffer_recv = bytearray(BUFFER_SIZE) 19 | buffer_temp = bytearray(BUFFER_SIZE) 20 | rv = 0 21 | count = 0 22 | length = 0 23 | i = 0 24 | datap = None 25 | 26 | if len(argv) == 2: 27 | if BUFFER_SIZE - 3 - 2 <= len(argv[1]): 28 | return 0 29 | buffer_send[3:3+len(argv[1])] = argv[1].encode() 30 | buffer_send[3+len(argv[1]):3+len(argv[1])+2] = b"\r\n" 31 | elif len(argv) == 1: 32 | buffer_send[3:] = b"at\r\n" 33 | else: 34 | return 0 35 | 36 | buffer_send[0] = 0xa4 37 | buffer_send[1] = (len(buffer_send[3:]) >> 8) & 0xff 38 | buffer_send[2] = len(buffer_send[3:]) & 0xff 39 | 40 | client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 41 | 42 | client_socket.bind(("", 0)) 43 | 44 | server_addr = (SERVER_IP, SERVER_PORT) 45 | 46 | client_socket.setblocking(False) 47 | 48 | print(f"RGMII-AT Client Up => {SERVER_IP}:{SERVER_PORT}") 49 | while True: 50 | try: 51 | client_socket.connect(server_addr) 52 | break 53 | except Exception as e: 54 | print(f"Can Not Connect To => {SERVER_IP}:{SERVER_PORT}") 55 | time.sleep(2) 56 | 57 | if True: 58 | rv = client_socket.send(buffer_send[:3+len(buffer_send[3:])]) 59 | print("\n\nsend:\n\n====================================> send all:", rv) 60 | print("==> length=", len(buffer_send[3:]), " head=0x%02x" % buffer_send[0]) 61 | print("\"" + buffer_send[3:].decode() + "\"") 62 | if rv != 3 + len(buffer_send[3:]): 63 | print("Send buf not complete") 64 | # return 0 65 | 66 | print("\n\nrecv:") 67 | while True: 68 | try: 69 | rv = client_socket.recv(BUFFER_SIZE) 70 | if len(rv) >= 3: 71 | print("\n\n====================================> recv all:", len(rv)) 72 | 73 | datap = rv 74 | while True: 75 | length = (datap[1] << 8) | (datap[2] & 0xff) 76 | buffer_temp[:length] = datap[3:3+length] 77 | 78 | print("==> length=", length, " head=0x%02x" % datap[0]) 79 | print("\"" + buffer_temp[:length].decode() + "\"") 80 | for i in range(length): 81 | # print("0x%02x " % buffer_temp[i], end="") 82 | pass 83 | print() 84 | 85 | rv = rv[length+3:] 86 | if len(rv) > 0: 87 | datap = rv 88 | if len(rv) < 0: 89 | print("client_socket recv not complete") 90 | 91 | if len(rv) <= 0: 92 | break 93 | 94 | buffer_recv[:] 95 | elif len(rv) > 0: 96 | print("client_socket recv error internal") 97 | break 98 | else: 99 | if not ql_rgmii_manager_server_fd_state(len(rv)): 100 | print("client_socket recv error") 101 | break 102 | except BlockingIOError: 103 | pass 104 | 105 | count += 1 106 | time.sleep(10 / 1000) 107 | 108 | if count == 300: 109 | break 110 | 111 | print() 112 | client_socket.close() 113 | return 0 114 | 115 | if __name__ == "__main__": 116 | import sys 117 | main(sys.argv) 118 | -------------------------------------------------------------------------------- /quectel_eth_at_client/quectel_reference_c/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the binary 2 | RGMII_AT_Client 3 | -------------------------------------------------------------------------------- /quectel_eth_at_client/quectel_reference_c/RGMII_AT_Client.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | #define SERVER_IP "192.168.225.1" 10 | #define SERVER_PORT 1555 11 | #define BUFFER_SIZE 2048*4 12 | 13 | int ql_rgmii_manager_server_fd_state(int n) 14 | { 15 | if(n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) 16 | { 17 | return 1; 18 | } 19 | if( n < 0 && (errno == EINTR || errno == EINPROGRESS)) 20 | { 21 | return 2; 22 | } 23 | else 24 | { 25 | return 0; 26 | } 27 | } 28 | 29 | int main(int argc, char **argv) 30 | { 31 | char buffer_send[BUFFER_SIZE] = {0}; 32 | char buffer_recv[BUFFER_SIZE] = {0}; 33 | char buffer_temp[BUFFER_SIZE] = {0}; 34 | int rv = 0; 35 | int count = 0; 36 | int len = 0; 37 | int i = 0; 38 | char * datap = NULL; 39 | 40 | if(argc == 2) 41 | { 42 | if(BUFFER_SIZE-3-2 <= strlen(argv[1])) return 0; 43 | memcpy(buffer_send+3, argv[1], strlen(argv[1])); 44 | memcpy(buffer_send+3+strlen(argv[1]), "\r\n", 2); 45 | } 46 | else if(argc == 1) 47 | snprintf(buffer_send+3, BUFFER_SIZE-3, "at\r\n"); 48 | else 49 | return 0; 50 | 51 | 52 | buffer_send[0] = 0xa4; 53 | buffer_send[1] = (uint8_t)((strlen(buffer_send+3) & (0xff00))>>8); 54 | buffer_send[2] = (uint8_t)(strlen(buffer_send+3) & (0x00ff)); 55 | 56 | 57 | struct sockaddr_in client_addr; 58 | memset(&client_addr, 0, sizeof(client_addr)); 59 | client_addr.sin_family = AF_INET; 60 | client_addr.sin_addr.s_addr = htons(INADDR_ANY); 61 | client_addr.sin_port = htons(0); 62 | 63 | int client_socket = socket(AF_INET,SOCK_STREAM,0); 64 | if( client_socket < 0) 65 | { 66 | printf("Create Socket Failed!\r\n"); 67 | return 0; 68 | } 69 | 70 | if( bind(client_socket,(struct sockaddr*)&client_addr,sizeof(client_addr))) 71 | { 72 | printf("Client Bind Port Failed!\r\n"); 73 | return 0; 74 | } 75 | 76 | struct sockaddr_in server_addr; 77 | memset(&server_addr, 0, sizeof(server_addr)); 78 | server_addr.sin_family = AF_INET; 79 | if(inet_aton(SERVER_IP, &server_addr.sin_addr) == 0) 80 | { 81 | printf("Server IP Address Error!\r\n"); 82 | return 0; 83 | } 84 | server_addr.sin_port = htons(SERVER_PORT); 85 | socklen_t server_addr_length = sizeof(server_addr); 86 | 87 | //set_non_blocking_mode client_socket 88 | fcntl(client_socket, F_SETFL, fcntl(client_socket, F_GETFL, 0) | O_NONBLOCK); 89 | 90 | 91 | printf("RGMII-AT Client Up => %s:%d\r\n", SERVER_IP, SERVER_PORT); 92 | while(1) 93 | { 94 | if(connect(client_socket,(struct sockaddr*)&server_addr, server_addr_length) >= 0) 95 | { 96 | break; 97 | } 98 | printf("Can Not Connect To => %s:%d\r\n", SERVER_IP, SERVER_PORT); 99 | sleep(2); 100 | } 101 | 102 | if(1) 103 | { 104 | rv = send(client_socket, buffer_send, (3+(int)strlen(buffer_send+3)),0); 105 | printf("\r\n\r\nsend:\r\n\r\n====================================> send all:%d\r\n==> len=%d head=0x%02x\r\n\"%s\"\r\n", 106 | rv, (int)strlen(buffer_send+3), (uint8_t)buffer_send[0], buffer_send+3); 107 | if(rv != (3+(int)strlen(buffer_send+3))) 108 | { 109 | printf("Send buf not complete\r\n"); 110 | //return 0; 111 | } 112 | } 113 | 114 | printf("\r\n\r\nrecv:"); 115 | while(1) 116 | { 117 | rv = recv(client_socket, buffer_recv, BUFFER_SIZE, 0); 118 | if(rv >= 3) 119 | { 120 | printf("\r\n\r\n====================================> recv all:%d", rv); 121 | 122 | datap = buffer_recv; 123 | do 124 | { 125 | len = (((uint16_t)((uint8_t)*(datap+1))<<8) | ((uint16_t)((uint8_t)*(datap+2)) & (0x00ff))); 126 | memset(buffer_temp, 0, sizeof(buffer_temp)); 127 | memcpy(buffer_temp, datap+3, len); 128 | 129 | printf("\r\n==> len=%d head=0x%02x\r\n\"%s\"\r\n", len, (uint8_t)*(datap), buffer_temp); 130 | for(i=0; i0) 138 | datap = buffer_recv+3+len; 139 | if(rv<0) 140 | printf("client_socket recv not complete\r\n"); 141 | 142 | }while(rv > 0); 143 | 144 | memset(buffer_recv, 0, sizeof(buffer_recv)); 145 | 146 | } 147 | else if(rv > 0) 148 | { 149 | printf("client_socket recv error internal\r\n"); 150 | break; 151 | } 152 | else 153 | { 154 | if(!ql_rgmii_manager_server_fd_state(rv)) 155 | { 156 | printf("client_socket recv error\r\n"); 157 | break; 158 | } 159 | 160 | } 161 | 162 | count++; 163 | usleep(10*1000); 164 | 165 | if(count == 300) 166 | { 167 | break; 168 | } 169 | 170 | } 171 | printf("\r\n"); 172 | close(client_socket); 173 | return 0; 174 | } -------------------------------------------------------------------------------- /quectel_eth_at_client/quectel_rgmii_at_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import socket 4 | import time 5 | import errno 6 | from errno import EAGAIN, EWOULDBLOCK, EINPROGRESS, EINTR 7 | import argparse 8 | 9 | """ 10 | Client for Quectel's QETH ETH_AT port on modems acting as a PCIe master with an ethernet port 11 | 12 | This is a modified version of a direct ChatGPT port of RGMII_AT_Client.c to python. If the socket 13 | communication can be simplified that would be awesome! 14 | 15 | Basic packet format, both sending and receiving: 16 | [identifier byte][two bytes for length][content][\r\n] 17 | 18 | For sending packets, it appears the identifier byte needs to be 0xa4. 19 | 20 | When receiving packets, it appears that the modem sends: 21 | 0xe0 with the RGMII_ATC_READY packet 22 | 0xa0 with the actual command output (both printing the command again, and the response.) 23 | 24 | The --debug flag will print the parsing of the packets along with the contents.. IE: 25 | 26 | ====================================> recv all: 113 27 | ==> length= 110 head=0xa0 28 | 29 | +QENG: "servingcell","NOCONN","NR5G-SA","FDD",313,340,03865B04C,583,5B01,401050,70,4,-110,-14,11,0,- 30 | 31 | OK 32 | 33 | The total length is 113 including the three header bytes, the length bytes are 110, and the initial byte is 0xa0. 34 | 35 | """ 36 | 37 | BUFFER_SIZE = 2048 * 4 38 | 39 | def ql_rgmii_manager_server_fd_state(n): 40 | if n == -1 and (errno == EAGAIN or errno == EWOULDBLOCK): 41 | return 1 42 | if n < 0 and (errno == EINTR or errno == EINPROGRESS): 43 | return 2 44 | else: 45 | return 0 46 | 47 | def main(args): 48 | SERVER_IP = args.modem_ip 49 | SERVER_PORT = args.modem_port 50 | DEBUG = args.debug 51 | 52 | buffer_send = bytearray(BUFFER_SIZE) 53 | buffer_recv = bytearray(BUFFER_SIZE) 54 | buffer_temp = bytearray(BUFFER_SIZE) 55 | rv = 0 56 | count = 0 57 | length = 0 58 | i = 0 59 | datap = None 60 | 61 | # If the buffer size is too small for the command, return? 62 | if BUFFER_SIZE - 3 - 2 <= len(args.at_command): 63 | return 0 64 | 65 | # Add the AT command plus \r\n to the send buffer starting at the fourth byte 66 | buffer_send[3:3+len(args.at_command)] = args.at_command.encode() 67 | buffer_send[3+len(args.at_command):3+len(args.at_command)+2] = b"\r\n" 68 | 69 | # First byte is always 0xa4 70 | buffer_send[0] = 0xa4 71 | 72 | # Second and third byte are the upper and lower bits of the full length of the command 73 | buffer_send[1] = (len(buffer_send[3:]) >> 8) & 0xff 74 | buffer_send[2] = len(buffer_send[3:]) & 0xff 75 | 76 | 77 | client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 78 | 79 | #client_socket.bind(("", 0)) 80 | 81 | server_addr = (SERVER_IP, SERVER_PORT) 82 | 83 | client_socket.setblocking(False) 84 | 85 | if DEBUG: 86 | print(f"RGMII-AT Client Up => {SERVER_IP}:{SERVER_PORT}") 87 | while True: 88 | try: 89 | client_socket.connect(server_addr) 90 | break 91 | except BlockingIOError as e: 92 | # This usually happens? 93 | pass 94 | except Exception as e: 95 | print(f"Can Not Connect To => {SERVER_IP}:{SERVER_PORT}") 96 | print(e) 97 | time.sleep(2) 98 | 99 | if True: 100 | rv = client_socket.send(buffer_send[:3+len(buffer_send[3:])]) 101 | if DEBUG: 102 | print("\n\nsend:\n\n====================================> send all:", rv) 103 | print("==> length=", len(buffer_send[3:]), " head=0x%02x" % buffer_send[0]) 104 | #print("SENDING: " + "\"" + buffer_send[3:].decode() + "\"") 105 | print("SENDING: " + buffer_send[3:].decode()) 106 | if rv != 3 + len(buffer_send[3:]): 107 | print("Send buf not complete") 108 | # return 0 109 | 110 | print("\nReceived:") 111 | while True: 112 | try: 113 | rv = client_socket.recv(BUFFER_SIZE) 114 | if len(rv) >= 3: 115 | 116 | datap = rv 117 | while True: 118 | length = (datap[1] << 8) | (datap[2] & 0xff) 119 | buffer_temp[:length] = datap[3:3+length] 120 | 121 | startbyte = "0x%02x" % datap[0] 122 | 123 | # The 'RGMII_ATC_READY is delivered with a start byte of 0xe0. It's there on every request, 124 | # so we don't need to print this. Might be good to check for it to validate that the protocol 125 | # is working properly, though. So, DEBUG will print it. 126 | if (DEBUG or not int(startbyte, 16) == 0xe0): 127 | # Headers if DEBUG.. 128 | if DEBUG: 129 | print("\n\n====================================> recv all:", len(datap)) 130 | print("==> length=", length, " head=0x%02x" % datap[0]) 131 | 132 | print(buffer_temp[:length].decode()) 133 | 134 | rv = rv[length+3:] 135 | if len(rv) > 0: 136 | datap = rv 137 | if len(rv) < 0: 138 | print("client_socket recv not complete") 139 | 140 | if len(rv) <= 0: 141 | break 142 | 143 | buffer_recv[:] 144 | elif len(rv) > 0: 145 | print("client_socket recv error internal") 146 | break 147 | else: 148 | if not ql_rgmii_manager_server_fd_state(len(rv)): 149 | print("client_socket recv error") 150 | break 151 | except BlockingIOError: 152 | pass 153 | 154 | count += 1 155 | time.sleep(10 / 1000) 156 | 157 | # This is kind of a lame way to do it. Just iterates X times then bombs. 158 | # TODO: Watch how long it's been since the last response, and kill it more quickly. 159 | if count == 1000: 160 | break 161 | 162 | print() 163 | client_socket.close() 164 | return 0 165 | 166 | if __name__ == "__main__": 167 | argparser = argparse.ArgumentParser(description="Execute AT commands over ethernet with a Quectel RM5xx modem") 168 | argparser.add_argument( 169 | "--modem-ip", 170 | type=str, 171 | default="192.168.225.1", 172 | required=False, 173 | help="Modem IP Address", 174 | dest="modem_ip", 175 | ) 176 | argparser.add_argument( 177 | "--modem-port", 178 | type=int, 179 | default=1555, 180 | required=False, 181 | help="Modem Port", 182 | dest="modem_port", 183 | ) 184 | argparser.add_argument( 185 | "--at-command", 186 | type=str, 187 | default="ATI", 188 | required=False, 189 | help="AT Command to execute", 190 | dest="at_command", 191 | ) 192 | argparser.add_argument( 193 | "--debug", 194 | default=False, 195 | action="store_true", 196 | help="Print additional protocol debugging information", 197 | ) 198 | 199 | args = argparser.parse_args() 200 | 201 | main(args=args) 202 | --------------------------------------------------------------------------------