├── .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 |
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 | 
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 |
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 |
--------------------------------------------------------------------------------