Skip to content

Instantly share code, notes, and snippets.

@andrewmackrodt
Last active June 3, 2024 21:42
Show Gist options
  • Save andrewmackrodt/71aab86065773abd05a22f8dcf07a74e to your computer and use it in GitHub Desktop.
Save andrewmackrodt/71aab86065773abd05a22f8dcf07a74e to your computer and use it in GitHub Desktop.
GL-X3000 AT Command Tool
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>GL-X3000 AT Command Tool</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css">
<style type="text/css">
body {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
code {
white-space: normal;
}
pre {
padding: 2rem;
}
</style>
<script>
window.config = {
auth: {
password: 'PUT_YOUR_PASSWORD_HERE',
sid: null,
username: 'root',
},
modem: {
bus: '0001:01:00.0',
port: '/dev/mhi_DUN',
},
router: {
ip: '192.168.8.1',
},
}
window.module = {
exports: { },
}
</script>
</head>
<body>
<h1>GL-X3000 AT Command Tool</h1>
<p>
This page must be run in a browser where CORS has been disabled or requests to the router will fail. A temporary
browser profile can be used with CORS disabled, e.g. for Google Chrome run:
<code>'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' --user-data-dir="$(mktemp -d)" --disable-web-security</code>
</p>
<!--
<h3 class="title">Router</h3>
<fieldset>
<div>
<label for="shortcut_select"><span><b>IP Address</b></span></label>
<input type="text" name="ipaddr" id="ipaddr_input" value="192.168.8.1">
</div>
<div>
<label for="shortcut_select"><span><b>Username</b></span></label>
<input type="text" name="username" id="username_input" value="root">
</div>
<div>
<label for="shortcut_select"><span><b>Password</b></span></label>
<input type="password" name="password" id="password_input">
</div>
</fieldset>
<h3 class="title">Command</h3>
-->
<form>
<fieldset>
<div>
<label for="shortcut_select"><span><b>Shortcut</b></span></label>
<select name="shortcut_select" id="shortcut_select">
<!-- Default commands -->
<option value='AT+GSN' selected>Request IMEI</option>
<option value='AT+QCCID'>Request QCCID</option>
<option value='AT+CIMI'>Request IMSI</option>
<option value='AT+CSQ'>Check Signal Quality</option>
<option value='AT&F0'>Reset modem</option>
<option value='AT+COPS?'>Operator Names</option>
<option value='AT+CPIN?'>Request SIM card status</option>
<!-- Custom commands: AT+QENG -->
<option value='AT+QENG="servingcell"'>Get Serving Cell</option>
<option value='AT+QENG="neighbourcell"'>Get Neighbour Cell</option>
<!-- Custom commands: AT+QNWLOCK -->
<option value='AT+QNWLOCK="common/4g"'>Get 4G Cell Lock</option>
<option value='AT+QNWLOCK="common/5g"'>Get 5G Cell Lock</option>
<!-- Custom commands: AT+QNWPREFCFG -->
<option value='AT+QNWPREFCFG="mode_pref"'>Get mode pref</option>
<option value='AT+QNWPREFCFG="nr5g_disable_mode"'>Get NR5G disable mode</option>
</select>
<label for="command_input"><span><b>AT Command</b></span></label>
<input type="text" name="command_input" id="command_input">
</div>
<div>
<button type="submit" id="command_submit"><span>Send Command</span></button>
</div>
</fieldset>
</form>
<pre id="command_output"></pre>
<script type="module">
import 'https://cdn.jsdelivr.net/npm/[email protected]/md5.min.js'
const { crypt, fromBytes } = window.module.exports
let id = 1
async function rpc(method, params) {
return await new Promise((resolve, reject) => {
fetch(`http://${window.config.router.ip}/rpc`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
jsonrpc: '2.0',
id: id++,
method,
params,
})
})
.then(async res => {
const text = await res.text()
const json = JSON.parse(text)
resolve(json.result)
})
.catch(err => reject(err))
})
}
async function getChallenge() {
return await rpc('challenge', { username: 'root' })
}
async function doLogin() {
const challenge = await getChallenge()
const login = await rpc('login', {
hash: fromBytes(
[
window.config.auth.username,
crypt(window.config.auth.password, challenge.salt),
challenge.nonce,
].join(':')
).toHex(),
username: window.config.auth.username,
})
window.config.auth.sid = login.sid
console.log(`logged in as username ${window.config.auth.username} sid ${login.sid}`)
}
async function doLoginCached() {
if ( ! window.config.auth.sid) {
await doLogin()
}
}
async function doSendAtCommand(command) {
await doLoginCached()
const res = await rpc('call', [
window.config.auth.sid,
'modem',
'send_at_command',
{
bus: window.config.modem.bus,
port: window.config.modem.port,
command,
}
])
let response = res
let status = '-'
if (response !== null && typeof response === 'object' && 'response' in response) {
response = res.response
}
if (typeof response === 'string') {
response = res.response.trim().split('\r\n')
status = response.pop()
response = response.join('\r\n').trim()
}
if (status !== 'OK') {
throw new Error(`AT command failed: ${status} - ${response}`)
}
return response
}
const shortcutSelectElement = document.getElementById('shortcut_select')
const commandInputElement = document.getElementById('command_input')
const commandSubmitElement = document.getElementById('command_submit')
const outputElement = document.getElementById('command_output')
shortcutSelectElement.addEventListener('change', e => commandInputElement.value = e.target.value)
shortcutSelectElement.dispatchEvent(new Event('change'))
commandSubmitElement.addEventListener('click', async e => {
e.preventDefault()
const command = commandInputElement.value.trim()
if ( ! command || command.match(/^AT\+[A-Z]/i) === null) {
alert('ERROR: command must begin AT+ and contain at least one character')
return
}
try {
outputElement.innerText = await doSendAtCommand(command)
} catch (e) {
alert(`ERROR: ${e}`)
}
})
</script>
</body>
</html>
#!/usr/bin/env bash
cd "$(cd "$(dirname "$(readlink -f "$0")")" && pwd -P)"
declare -a chromeArgs=(
--disable-back-forward-cache
--disable-background-networking
--disable-background-timer-throttling
--disable-backgrounding-occluded-windows
--disable-breakpad
--disable-client-side-phishing-detection
--disable-component-extensions-with-background-pages
--disable-component-update
--disable-default-apps
--disable-dev-shm-usage
--disable-extensions
--disable-features="ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding"
--disable-field-trial-config
--disable-hang-monitor
--disable-ipc-flooding-protection
--disable-popup-blocking
--disable-prompt-on-repost
--disable-renderer-backgrounding
--disable-search-engine-choice-screen
--disable-web-security
--new-window
--no-default-browser-check
--no-first-run
--no-service-autorun
--password-store="basic"
--test-type
--unsafely-disable-devtools-self-xss-warnings
--use-mock-keychain )
userDataDir=$(mktemp -d)
if [[ "$userDataDir" == "" ]] || [[ ! -d "$userDataDir" ]]; then
echo "ERROR unable to create temporary directory" >&2
exit 1
fi
cleanup() {
rm -rf "$userDataDir"
}
trap cleanup exit
chromeArgs+=( --user-data-dir="$userDataDir" )
chromeArgs+=( --app="file://$PWD/glx3000-at-tool.html" )
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' "${chromeArgs[@]}" >/dev/null 2>&1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment