SCK-915 Explorer Kit

Developer Guide
Version 2.0  |  May 2026  |  SCK-915 Explorer Kit Documentation

Quick Reference — Search Tags

Every cross-reference in the firmware source uses a [SCK-DEV: TAG] comment. Search for any tag in the codebase to jump directly to the relevant section:

TagTopicSection
SCK-DEV: BOARD_CONFIGHardware configuration overview2.1
SCK-DEV: RF_FRONTENDCC1190 PA/LNA control2.2
SCK-DEV: RF_CONFIG915MHz register values2.3
SCK-DEV: TX_POWERPower mode / bench vs field2.4
SCK-DEV: BOOTLOADERBootloader timeout2.5
SCK-DEV: ADD_COMMANDAdding new commands3.1
SCK-DEV: CHUNKINGChunking large responses3.2
SCK-DEV: TIMINGPrototype board timing issues3.3
SCK-DEV: ESP_FRAMINGUART framing protocol3.4
SCK-DEV: RESPONSE_FORMATResponse string conventions3.5
SCK-DEV: BEACONAutonomous GPS beacon3.6
SCK-DEV: SD_FLIGHT_LOGSD card flight log format3.7

1.0 System Overview

1.1 Architecture

Ground Station (Windows C#)
    ↕ USB / Serial — COM port — 115200 baud — ESP framing
SCK-915 RF Board (CC1110 + CC1190)        ← ground unit
    ↕ 915MHz RF — 7.4 kBaud — 2-FSK + FEC
SCK-915 RF Board (CC1110 + CC1190)        ← remote / payload unit
    ↕ UART0 — 115200 baud — ESP framing
SCK-PBL-1 Payload Board (Raspberry Pi Pico)
    ├── OV2640 Camera    (SPI0 + I2C0)
    ├── MicroSD Card     (SPI0)
    ├── GPS NEO-6M       (UART1)
    └── BMP581 Altimeter (I2C1)

The ground unit RF board relays packets transparently between the ground station and the remote unit. Only the remote unit requires the custom PICO_MSG handler in board.c. This mirrors the original OpenLST Explorer Kit architecture where Board 0001 was the transparent relay and Board 0004 ran the custom firmware.

1.2 Hardware Variants

BoardDescriptionStatus
SCK-915 PrototypeHand-wired jumper development boardDev/test only — see Section 3.3
SCK-915 RF BoardCC1110 + CC1190 RF front end PCBProduction
SCK-PBL-1 v1.0 PayloadPico + camera + GPS + BMP581 + SD PCBProduction — BMP581 altimeter
SCK-PBL-1 v1.1 PayloadPico + camera + GPS + MS5611 + SD PCBProduction — MS5611 altimeter

1.3 Firmware Components

FileLanguageRuns OnPurpose
board.hCCC1110Hardware configuration and defines
board.cCCC1110Board init and PICO_MSG relay
main.pyMicroPythonPicoPayload pipeline and command handlers (MS5611 active, BMP581 preserved as comments)
sdcard.pyMicroPythonPicoMicroPython SD card SPI driver

1.4 Repository Layout

openlst-master/
├── Makefile                    ← top-level build
├── open-lst/                   ← DO NOT MODIFY
│   ├── common/                 ← uart0.c uart1.c radio.c ...
│   ├── radio/                  ← main.c commands.c ...
│   └── bootloader/
└── radios/openlst_437/         ← YOUR CODE LIVES HERE
    ├── Board.mk                ← build config
    ├── board.h                 ← hardware defines + custom_commands declaration
    └── board.c                 ← board_init() + custom_commands()
Never modify files inside open-lst/ All customization goes in radios/openlst_437/. This keeps your changes clean and upstream updates safe.

2.0 Hardware Configuration Reference

2.1 Board Configuration — board.h [SCK-DEV: BOARD_CONFIG]

board.h configures the OpenLST firmware for SCK-915 hardware. Key defines:

DefineValuePurpose
F_CLK27000000Crystal frequency — must match physical crystal
CUSTOM_BOARD_INIT1Enables board_init() in board.c
BOARD_HAS_TX_HOOK1Enables board_pre_tx() macro
BOARD_HAS_RX_HOOK1Enables board_pre_rx() macro
ADCCFG_CONFIG0b00000011Enables AN0 and AN1 for voltage sense
RADIO_RANGING_RESPONDER1Enables ranging response
CUSTOM_COMMANDS1Enables custom command dispatch

Build flags (set in Board.mk)

openlst_437_CFLAGS := \
    -DCUSTOM_BOARD_INIT   \
    -DCUSTOM_COMMANDS     \
    -DUART0_ENABLED=1     \
    -I$(openlst_437_DIR)
Always use Clean + Build after any source change Stale .rel object files are the most common cause of mysterious firmware behavior that is difficult to trace. This is not optional — it is required.

2.2 CC1190 RF Front End [SCK-DEV: RF_FRONTEND]

The CC1190 provides PA and LNA functions between the CC1110 and antenna.

Physical connections

CC1190 PinSignalCC1110 PinControl Method
Pin 8 (PA_EN)PA enableP1.6 (GDO1)Automatic via IOCFG1
Pin 7 (LNA_EN)LNA enableP1.7 (GDO2)Automatic via IOCFG2
Pin 6 (HGM)High gain modeP2.0Software via _rf_high_gain

HGM power modes

_rf_high_gainHGM StateTotal Output PowerUse Case
0LOW~+10 dBmBench / indoor testing
1HIGH~+27 dBmField / HAB / LEO mission

GDO pins are configured using inverted power-down signals so the CC1190 enable lines go HIGH automatically when the corresponding stage is active. The firmware never manually toggles PA_EN or LNA_EN — the CC1110 radio hardware manages this through the IOCFG register settings.

2.3 RF Configuration — 915MHz [SCK-DEV: RF_CONFIG]

All register values were derived using SmartRF Studio 7 for the CC1110 with a 27MHz crystal.

ParameterValue
Base Frequency914.999512 MHz
Crystal27.000 MHz
Modulation2-FSK
Data Rate7.415 kBaud
Deviation3.708 kHz
RX Filter BW60.268 kHz

Frequency control word calculation

FREQ[23:0] = (Fcarrier / Fxtal) × 65536
           = (914999512 / 27000000) × 65536
           = 0x21E38D

RF_FREQ2 = 0x21, RF_FREQ1 = 0xE3, RF_FREQ0 = 0x8D

To change frequency

  1. Open SmartRF Studio 7, select CC1110
  2. Enter desired frequency and 27MHz crystal
  3. Export register values
  4. Update RF_FREQ2, RF_FREQ1, RF_FREQ0 in board.h
  5. Update RF_FSCTRL1 if moving to a different frequency band

2.4 TX Power Configuration [SCK-DEV: TX_POWER]

Power mode is set at flash time — not at runtime.

RF_PA_CONFIGCC1110 OutputTotal w/CC1190Firmware Build
0xC0~0 dBm~18 dBmBench / indoor (default)
0xC2~10 dBm~28 dBmField / HAB / LEO

The ground station patches RF_PA_CONFIG automatically when flashing bench or field firmware.

Critical There must be exactly one #define RF_PA_CONFIG line in board.h. The ground station patching tool searches for this exact string. Adding duplicates or moving this line will break the patching tool silently.

2.5 Bootloader Timeout [SCK-DEV: BOOTLOADER]

COMMAND_WATCHDOG_DELAY = 1350000 ≈ 600ms at 27MHz.

The OpenLST default (45000 ≈ 20ms) is too short for RF OTA flashing. The 600ms window gives the ground station time to detect the bootloader beacon and transmit a flash command before the bootloader times out and jumps to the application. Do not reduce below 1,000,000 for OTA-capable deployments.

After a direct serial flash, warm up the board with 2–3 get_telemetry commands before attempting OTA operations. This allows the RF synthesizer to stabilize fully.


3.0 Firmware Extension Reference

3.1 Adding New Commands [SCK-DEV: ADD_COMMAND]

Adding a new command requires changes in three places. The CC1110 is a transparent relay for all PICO_MSG (0x20) commands — it forwards any payload to the Pico regardless of sub-opcode, so board.c typically requires no changes.

Step 1 — Assign a sub-opcode in main.py

# Add to the sub-opcode constants at the top of main.py
CMD_MY_COMMAND = 0x0A   # Next available opcode after 0x09

Step 2 — Add handler in main.py command dispatch loop

elif sub_opcode == CMD_MY_COMMAND:
    try:
        # payload[0] = sub_opcode
        # payload[1:] = command-specific data
        param = payload[1:].decode('utf-8').strip()

        result = do_something(param)

        # Must fit in MAX_PAYLOAD (251 bytes)
        # For larger responses implement chunking — see Section 3.2
        msg = f"MYRESP:{result}"
        send_esp(msg.encode())
        blink(1)
        print(f"MY_COMMAND → {msg}")

    except Exception as e:
        send_esp(b"MYRESP:ERR:FAIL")
        print(f"MY_COMMAND error: {e}")

Step 3 — Add to C# ground station

// In CommandOpcode enum
MyCommand = 0x0A,

// Command method
private async Task MyCommandAsync() {
    if (!CheckConnected()) return;
    ushort hwid = ActiveHwid;
    ushort seq  = IncSeqNum();
    FlushRxQueue();
    WritePacket(OpenLstProtocol.BuildPacket(
        hwid, seq, 0x20, new byte[] { 0x0A }));
    var pkt = await WaitForReply(hwid, seq, 10000);
    if (pkt?.PicoPayload != null)
        Log($"Response: {pkt.PicoPayload}", Theme.Green);
    else
        Log("No response", Theme.Red);
}

Step 4 — board.c changes (only if needed)

Only modify board.c if your command needs a timeout longer than the standard 800ms (e.g. commands involving SD card writes):

// Add timeout constant
#define PICO_TIMEOUT_OUTER_MY_SLOW_CMD  300  // ~3 seconds

// Add sub-opcode check in custom_commands()
outer_max = (payload_len > 0 && cmd->data[0] == PICO_SUB_MY_SLOW_CMD)
            ? PICO_TIMEOUT_OUTER_MY_SLOW_CMD
            : PICO_TIMEOUT_OUTER;

Critical rules for board.c

RuleConsequence if violated
Never set reply->header.hwid, seqnum, or systemMemory corruption — RF stops working
Only set reply->header.commandIt is the only field custom_commands owns
Never use WATCHDOG_CLEAR macroCorrupts RF receive via register conflict
Never declare static __xdata buffersXDATA RAM overflow — board crashes
Use uint8_t/uint16_t only for countersuint32_t causes RAM overflow crash
Use nested loops for timeouts >65535uint16_t wraps silently — timeout too short
Always send ≥1 byte payloadZero-length ESP frames are silently rejected

3.2 Chunking Large Responses [SCK-DEV: CHUNKING]

The maximum RF packet payload is 251 bytes (MAX_PAYLOAD). Responses larger than this must be split into chunks.

Pattern A — Text List Chunking (used by CMD_LIST)

"LIST:file1.jpg,file2.jpg,"     ← first chunk — trailing comma
"LIST+:file3.jpg,file4.jpg,"    ← continuation — trailing comma
"LIST+:file5.jpg"               ← final chunk — NO trailing comma = end signal

The ground station accumulates chunks until a packet with no trailing comma arrives, then reassembles.

CHUNK = MAX_PAYLOAD - 8   # Reserve space for prefix
chunk_items = []
chunk_len   = 0
first_chunk = True

for item in items:
    entry = item + ","
    if chunk_len + len(entry) > CHUNK and chunk_items:
        prefix = "MYLIST:" if first_chunk else "MYLIST+:"
        send_esp((prefix + ",".join(chunk_items) + ",").encode())
        first_chunk = False
        chunk_items = [item]
        chunk_len   = len(item) + 1
    else:
        chunk_items.append(item)
        chunk_len += len(entry)

# Final chunk — no trailing comma signals end of list
if chunk_items:
    prefix = "MYLIST:" if first_chunk else "MYLIST+:"
    send_esp((prefix + ",".join(chunk_items)).encode())

Pattern B — Binary File Chunking (used by CMD_GET_INFO + CMD_GET_CHUNK)

Used for transferring binary files such as JPEG images and .sckflight logs.

Protocol sequence:

  1. Ground station sends CMD_GET_INFO with filename
  2. Pico responds "INFO:<filename>:<total_bytes>:<chunk_count>"
  3. Ground station sends CMD_GET_CHUNK for index 0, 1 … chunk_count-1
  4. Pico responds "CHUNK:<index>:<200 bytes binary>" for each
  5. Ground station reassembles in order

Chunk size: 200 bytes (CHUNK_SIZE) — leaves 51 bytes for the "CHUNK:65535:" prefix within the 251-byte limit.

# GET_INFO handler
size   = uos.stat(f"/sd/{filename}")[6]
chunks = (size + CHUNK_SIZE - 1) // CHUNK_SIZE
send_esp(f"INFO:{filename}:{size}:{chunks}".encode())

# GET_CHUNK handler
chunk_index = payload[1] | (payload[2] << 8)   # little-endian uint16
filename    = payload[3:].decode('utf-8').strip()
with open(f"/sd/{filename}", 'rb') as f:
    f.seek(chunk_index * CHUNK_SIZE)
    data = f.read(CHUNK_SIZE)
send_esp(f"CHUNK:{chunk_index}:".encode() + data)

3.3 Timing Issues — Prototype Board [SCK-DEV: TIMING]

Prototype board only This section applies to the hand-wired SCK-915 prototype board only. The SCK-PBL-1 production PCB resolves all of these issues through proper PCB routing and direct trace connections.

Issue 1 — SPI Bus Speed

Jumper wire capacitance and crosstalk limit the SPI bus to 400kHz on the prototype. The production target on SCK-PBL-1 is 1.32MHz (the sdcard.py design speed).

Symptoms: SD card mount fails, camera produces corrupt images, SD mount failed: in serial output.

# Prototype (400kHz — safe for jumper wires)
spi = SPI(0, baudrate=400000, ...)

# SCK-PBL-1 production (1.32MHz — sdcard.py design speed)
spi = SPI(0, baudrate=1320000, ...)

# Also update slow init in mount_sd():
# Prototype: baudrate=100000
# SCK-PBL-1: baudrate=400000

Issue 2 — UART Response Timing

Jumper wire noise can delay Pico UART responses enough to trigger the CC1110 timeout, causing intermittent NACKs even though the Pico completed the command correctly.

Symptoms: Commands NACK intermittently. Pico serial output shows command completed. Same command sometimes succeeds, sometimes fails.

#define PICO_TIMEOUT_OUTER  40    // ~800ms standard
#define PICO_TIMEOUT_OUTER  80    // ~1600ms for prototype

Issue 3 — SPI Bus Sharing (Camera + SD)

Camera and SD card share SPI0. Both CS pins must be HIGH before switching between devices. Missing a CS deselect causes data corruption on both devices.

# Always do this before switching between camera and SD
cam_cs(1); sd_cs(1)
time.sleep_ms(10)
# Then select the device you need

Issue 4 — Camera Initialization Blocking UART

Camera initialization over I2C takes several hundred milliseconds. Initializing at boot blocks UART0 and can cause the CC1110 to miss the first command after power-on.

Fix (already implemented) Camera init is deferred to the first idle timeout in the main loop, not at boot. Do not move init_camera() back to the startup sequence.

Issue 5 — Arducam Signal Integrity

The Arducam OV2640 requires direct wire connections with minimal capacitance. Breadboard connections add enough capacitance to cause intermittent SPI read failures on the FIFO readout. Use direct wired connections or the SCK-PBL-1 PCB traces. Do not use breadboard for camera SPI connections.

3.4 ESP Framing Protocol [SCK-DEV: ESP_FRAMING]

All UART0 communication between CC1110 and Pico uses ESP framing:

[0x22][0x69][length][payload bytes...]
  ↑      ↑      ↑         ↑
Start  Start  1 byte   1-251 bytes
byte0  byte1  payload  command data
              length

OpenLST packet structure inside the payload

ByteFieldNotes
0-1HWIDLittle-endian destination hardware ID
2-3SequenceLittle-endian seq number (16–64000)
4System0x01 = MSG_TYPE_RADIO_IN (required)
5OpcodeCommand opcode byte
6+DataCommand-specific payload
Critical Length must be ≥ 1. Zero-length ESP frames are silently rejected by the RX interrupt service routine.

MicroPython implementation

ESP_START_0 = 0x22
ESP_START_1 = 0x69
MAX_PAYLOAD = 251

def send_esp(payload_bytes):
    uart.write(bytes([ESP_START_0, ESP_START_1,
                      len(payload_bytes)]) + payload_bytes)

def recv_esp(timeout_ms=5000):
    S0, S1, SL, SP = 0, 1, 2, 3
    state, payload, remaining = S0, bytearray(), 0
    deadline = time.ticks_add(time.ticks_ms(), timeout_ms)
    while time.ticks_diff(deadline, time.ticks_ms()) > 0:
        if uart.any():
            b = uart.read(1)[0]
            if state == S0:
                if b == ESP_START_0: state = S1
            elif state == S1:
                if b == ESP_START_1:   state = SL
                elif b == ESP_START_0: state = S1
                else:                  state = S0
            elif state == SL:
                if 1 <= b <= 251:
                    remaining, payload, state = b, bytearray(), SP
                else: state = S0
            elif state == SP:
                payload.append(b); remaining -= 1
                if remaining == 0: return bytes(payload)
        else:
            time.sleep_ms(1)
    return None

3.5 Response String Format Conventions [SCK-DEV: RESPONSE_FORMAT]

All Pico responses follow this format: PREFIX:field1,field2,...

Complete response table

CommandOpcodeSuccess ResponseError Response
PING0x00PICO:ACK
READ_TEMP0x01TEMP:23.45C
SNAP0x02SNAP:OK:snap_001.jpg:24576SNAP:ERR:NO_SD / SNAP:ERR:NO_CAM
LIST0x03LIST:file1.jpg,file2.jpgLIST:ERR:NO_SD / LIST:EMPTY
GET_INFO0x04INFO:snap_001.jpg:24576:123INFO:ERR:NOTFOUND
GET_CHUNK0x05CHUNK:0:<200 bytes>CHUNK:ERR:FAIL
DELETE0x06DEL:OK:snap_001.jpgDEL:ERR:FAIL
GET_GPS0x07GPS:35.12,-97.56,1523,8,1,1013.25,1521.3,22.5
GET_BARO0x08BARO:1013.25,1521.3,22.5BARO:ERR:NOT_READY
BEACON_CTRL0x09BEACON:ON / BEACON:OFF

3.6 Autonomous GPS Beacon [SCK-DEV: BEACON]

The Pico transmits a fused GPS+barometric packet autonomously every 10 seconds when no command is being processed and the beacon is enabled.

Beacon interval: GPS_BEACON_INTERVAL_MS = 10000 (10 seconds)

Beacon format: GPS:lat,lon,gps_alt,sats,fix,hpa,baro_alt,temp_c

Controlling the beacon:

CMD_BEACON_CTRL (0x09) + 0x01 → enable
CMD_BEACON_CTRL (0x09) + 0x00 → disable
Tip Disable during high-rate file transfers to prevent beacon packets from interleaving with file chunk responses. Re-enable after the transfer. Every beacon packet is also written to the SD flight log — see Section 3.7.

3.7 SD Card Flight Log Format [SCK-DEV: SD_FLIGHT_LOG]

One .sckflight file is created per power cycle. Naming: FLT-001.sckflight, FLT-002.sckflight, etc.

File format (JSON)

{"flight_id":"FLT-001","hardware":"SCK-915+SCK-PBL-1","packets":[
{"t":0.0,"lat":0.0,"lon":0.0,"gps_alt":0.0,"sats":0,"fix":0,
 "baro_hpa":1013.25,"baro_alt":215.3,"baro_temp":22.5,"event":""},
{"t":10.1,"lat":35.123456,"lon":-97.654321,"gps_alt":1523.4,
 "sats":8,"fix":1,"baro_hpa":849.12,"baro_alt":1521.3,
 "baro_temp":18.2,"event":"SNAP:snap_001.jpg"}
],
"summary":{"total_packets":42,"flight_duration_s":420.0}}

Adding new fields

In write_flight_packet() in main.py:

line = (f'{sep}{{"t":{t},'
        f'"lat":{gps_fix["lat"]:.6f},'
        # existing fields...
        f'"my_new_field":{my_value},'   # ← add here
        f'"event":"{event}"}}\n')

Also update the ground station FlightReplayForm parser.


4.0 Payload Board Hardware Reference

4.1 SCK-PBL-1 GPIO Assignments

GPIOFunctionNotes
0UART0 TX → CC1110ESP framing to RF board
1UART0 RX ← CC1110ESP framing from RF board
2I2C1 SDA (MS5611)400kHz — same pins as previous BMP581
3I2C1 SCL (MS5611)400kHz
4I2C0 SDA (Camera)100kHz
5I2C0 SCL (Camera)100kHz
8UART1 TX → GPS9600 baud — UBX config only
9UART1 RX ← GPS9600 baud — NMEA stream in
10LED Camera (GREEN)Solid = camera initialized OK
11LED SD Card (GREEN)Solid = SD mounted OK
12LED GPS (BLUE)Slow flash = searching / Solid = fix
13LED Altimeter (YELLOW)Solid = BMP581 initialized OK
14LED Fault (RED)Solid = any subsystem failed
15Camera CSActive LOW — deselect before SD access
16SD Card CSActive LOW — deselect before camera access
17SPI0 MOSIShared camera + SD
18SPI0 SCLKShared camera + SD
19SPI0 MISOShared camera + SD
20–24Free GPIOAvailable for expansion payloads
25Onboard LEDBoot confirmation blinks
26–28Free GPIOAvailable for expansion payloads

4.2 Expansion Headers — 2×20 Pin Breakout

The SCK-PBL-1 payload board includes two 2×20 pin headers that break out all Raspberry Pi Pico GPIO pins — including pins already in use by the on-board subsystems. This allows researchers and developers to attach additional payload hardware without requiring a new PCB revision for each mission variant.

J_EXP1 and J_EXP2 — full Pico pin breakout

J_DEBUG — dedicated debug header

Free GPIOs for expansion: 20–24 and 26–28 (seven free GPIO pins)

When attaching expansion hardware SPI0 can be shared — add a new CS GPIO and manage it explicitly. I2C buses support multiple devices — ensure unique I2C addresses. Do not drive in-use pins as outputs without disconnecting the on-board subsystem first.

5.0 C# Ground Station Extension Reference

5.1 Key Methods

MethodDescription
ActiveHwidProperty: current HWID from header bar
IncSeqNum()Increment and return next sequence number
FlushRxQueue()Discard queued received packets before sending
WritePacket(byte[])Send raw bytes to serial port
WaitForReply(hwid,seq,ms)Async: wait for matching ack/response
Log(message, color)Append line to main log panel
LogTx(message, hwid)Log transmitted packet in yellow TX format
CheckConnected()Returns false and logs error if not connected
MkButton(text,x,y,w,fg,bg)Create styled flat button
MkLabel(text,x,y,w,color,font)Create styled label
MkSectionLabel(text,x,y)Create cyan section divider
MkTab(text)Create styled tab page

5.2 Adding a Custom Command (C# Template)

private async Task MyCommandAsync() {
    if (!CheckConnected()) return;
    ushort hwid = ActiveHwid;
    ushort seq  = IncSeqNum();
    FlushRxQueue();
    WritePacket(OpenLstProtocol.BuildPacket(
        hwid, seq, 0x20, new byte[] { 0x0A }));  // sub-opcode 0x0A
    var pkt = await WaitForReply(hwid, seq, 10000);
    if (pkt?.PicoPayload != null)
        Log($"Response: {pkt.PicoPayload}", Theme.Green);
    else
        Log("No response", Theme.Red);
}

5.3 Opcode Reference

OpcodeNameDescription
0x10common_msg_ackAcknowledge response
0xFFcommon_msg_nackNot-acknowledge response
0x11common_msg_asciiASCII debug text
0x12radio_msg_rebootReboot (optional 4-byte postpone_sec)
0x13radio_msg_get_timeRequest RTC time
0x14radio_msg_set_timeSet RTC time
0x17radio_msg_get_telemRequest telemetry
0x18radio_msg_telemTelemetry response (83 bytes)
0x19radio_msg_get_callsignRequest callsign
0x1Aradio_msg_set_callsignSet callsign (8 bytes)
0x20PICO_MSGForward payload to Pico via UART0

6.0 Build and Flash Reference

6.1 Building CC1110 Firmware

# Bench firmware (safe indoor power — default)
mingw32-make openlst_437_radio RF_PA_CONFIG=0xC0

# Field firmware (full mission power)
mingw32-make openlst_437_radio RF_PA_CONFIG=0xC2

Or use the ground station "Flash Firmware" button which handles build selection and patching automatically.

Always Clean + Build Never trust an incremental build after modifying board.h or board.c.

6.2 Flashing Pico Firmware

# Using mpremote
mpremote cp main.py :main.py
mpremote cp sdcard.py :sdcard.py

Or hold BOOTSEL on power-up and drag-and-drop files to the USB mass storage drive.

6.3 OTA Firmware Update (CC1110)

  1. Power cycle the SCK-915 board
  2. Ground station detects bootloader beacon within the 600ms window
  3. Click "Flash Firmware" — select bench or field build
  4. Board reflashes and restarts with new firmware
  5. Send 2–3 get_telemetry commands after OTA flash before other operations

The 600ms window is controlled by COMMAND_WATCHDOG_DELAY in board.h.


7.0 Hard-Won Lessons

These lessons were accumulated during development of the SCK-915 system starting from the OpenLST Explorer Kit prototype. Each one cost real debugging time. Read them before starting any firmware work.

#LessonImpact
L1UART1 is the ground station interface — use UART0 for PicoCritical
L2Custom commands must be in board.c — not a separate fileCritical
L3Never reinitialize reply->header in custom_commandsCritical
L4WATCHDOG_CLEAR corrupts RF receive — never use itHigh
L5uint32_t causes XDATA RAM crash — use uint16_t onlyHigh
L6uint16_t wraps at 65535 — use nested loops for long timeoutsHigh
L7Zero-length ESP payload silently rejected by RX ISRMedium
L8SD mount corrupts SPI state for Arducam — reinit SPI after mountHigh
L9Arducam needs direct wire connection — breadboard adds too much capacitanceMedium
L10Camera init blocks UART — defer to first idle timeout, never at bootMedium
L11Ground unit must run original unmodified OpenLST firmwareHigh
L12After direct serial flash: send 2–3 get_telemetry before RF opsMedium
L13Always Clean + Build — stale .rel files cause silent firmware bugsHigh
L14SPI bus shared between camera and SD — always deselect both CS before switchingHigh
L15Prototype board SPI limited to 400kHz — jumper wire noise corrupts at higher speedsMedium
L16PICO_TIMEOUT values tuned for production PCB — double them on prototype boardMedium
L17RF_PA_CONFIG must appear exactly once in board.h — ground station patching tool requires itHigh
L18Bootloader window is 600ms — ground station must detect beacon and respond within this windowHigh