Adding support for Dataremote CDS-9010 (unlocked and AT&T)

Hey all, figured I'd document this since I got OpenWrt running on the CDS-9010. Hoping someone more competent than I can get this running.

What is it?

The CDS-9010 is a POTS-over-LTE box and 4G router. Available for $25-50 used. Hardware-wise it's basically a standard-ish MT7621 router with an LTE modem slot. Specs: MT7621A @ 880MHz, 128MB RAM, 32MB flash, MT7615 WiFi, Quectel EC25-AF 4G, 2x FXS ports, battery backup.

What works?

The Unielec U7621-06-32M firmware boots and runs! WiFi, ethernet work. LTE card connects but disconnects about 30 seconds after the device boots. It also has issues booting the image off flash, but it will boot over TFTP (you need serial access)

What doesn't work:

LEDs and buttons are mapped wrong (different GPIOs than Unielec). I do not know how to do DTS, but I tried. LTE card also does not work.

Happy to answer questions if anyone else has one of these.

Also side note: the 4G card is Unlocked and works with AT&T, FirstNet, T-Mobile USA, US Cellular, and Verizon Wireless, plus MVNOs using these networks, or more generally, in ITU region 2. (bands 2/4/5/12/13/14/66/71 are supported)

I've created a WikiDevi page with details from my device. I assume you have determined the pin out for the serial connection. If so can you share the details? Also is it using the standard 115200 rate used by most MediaTek devices?


Serial access. Pin 1 starts closest to blue ethernet port, pin 3 (yellow) on header is TX and pin 4 (white) is RX. I used the SIM port as ground point.

Dump (required for default password):
Connect router to PC and serial adapter, press 4 at the U-Boot menu. Have the router sitting at a blank MT7621:#/ uboot prompt, then run dump_flash,py on the serial port - it will dump the stock NAND which can be restored with getimage over TFTP.

Password:
cat CDS-dump1.bin | grep SUPER will give you the super web password.

dump_flash.py:

#!/usr/bin/env python3
import serial
import sys
import time
import re

CHUNK_SIZE = 0x20000  # 128KB per read
SERIAL_PORT = "/dev/ttyUSB0"
BAUD = 115200

def parse_hex_output(output):
    """Extract hex bytes from spi read output"""
    # spi read outputs: "XX XX XX XX ..." 
    # Filter out the command echo and prompt lines
    hex_bytes = []
    lines = output.split("\n")
    for line in lines:
        line = line.strip()
        # Skip empty, prompts, command echoes
        if not line or line.startswith("MT7621") or line.startswith("spi ") or line.startswith("read len"):
            continue
        # Try to parse as hex bytes
        parts = line.split()
        for p in parts:
            p = p.strip()
            if len(p) <= 2 and all(c in "0123456789abcdefABCDEF" for c in p):
                if len(p) == 1:
                    p = "0" + p
                try:
                    hex_bytes.append(int(p, 16))
                except:
                    pass
    return bytes(hex_bytes)

def dump_flash(start_offset, total_size, output_file):
    ser = serial.Serial(SERIAL_PORT, BAUD, timeout=30)
    time.sleep(0.5)
    
    # Clear any pending data
    ser.reset_input_buffer()
    
    with open(output_file, "wb") as f:
        offset = start_offset
        end = start_offset + total_size
        
        while offset < end:
            chunk = min(CHUNK_SIZE, end - offset)
            cmd = f"spi read 0x{offset:X} 0x{chunk:X}\n"
            print(f"Reading 0x{offset:X} - 0x{offset+chunk:X}...", end=" ", flush=True)
            
            ser.write(cmd.encode())
            time.sleep(0.1)
            
            # Wait for data - 128KB takes a while
            output = b""
            deadline = time.time() + 60  # 60s timeout per chunk
            while time.time() < deadline:
                if ser.in_waiting:
                    output += ser.read(ser.in_waiting)
                    time.sleep(0.1)
                else:
                    # Check if we got the prompt back
                    if b"MT7621 #" in output[-50:]:
                        break
                    time.sleep(0.2)
            
            data = parse_hex_output(output.decode("latin-1"))
            f.write(data)
            print(f"got {len(data)} bytes")
            
            if len(data) != chunk:
                print(f"WARNING: expected {chunk} bytes, got {len(data)}")
            
            offset += chunk
    
    ser.close()
    print(f"Done! Wrote {output_file}")

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print("Usage: python dump_flash.py <start_hex> <size_hex> <output.bin>")
        print("Example: python dump_flash.py 0x0 0x2000000 full_dump.bin")
        sys.exit(1)
    
    start = int(sys.argv[1], 16)
    size = int(sys.argv[2], 16)
    outfile = sys.argv[3]
    
    dump_flash(start, size, outfile)

Couldn't get the grep command to work as it always responded with "CDS9010.bin: binary file matches". Using strings command I located the following:

DBID_SUPER_WEB_PASSWORD=2eDTN8Gi8bT2

Is that what I was meant to find?

Is this mean to allow log in via serial? If not how do I make use of the credentials?

The serial console disables after U-BOOT, but you can login with username superadmin and that password at 192.168.25.1 over ssh or the browser web UI. It's pretty well featured, but I would like to be able to help test a real OS.