Link Search Menu Expand Document

Seeedstudio - SenseCap-S2103

The SenseCap S2103 is a LoRaWAN indoor/outdoor sensor to measure temperature, humidity and CO2


Table of contents

  1. Specifications
  2. Documents/Links
  3. Ordering Info
  4. Button Actions, Modes and LED States
  5. Adding the Device to TTN
  6. Settings (mandatory!)
  7. Change Settings over Downlink messages
    1. Uplink Interval
  8. Payload Decoder

Specifications

  • indoor/outdoor device
  • Price ca. CHF 145.- (09.08.2023)
  • Sensors
    • Temperature, -40 … +85 [°C], ± 0.2 °C, Resolution 0.01 °C, Long term drift < 0.03 °C/year
    • relative Humidity, 0 … 100[%rH], ± 1.8, Resolution 0.01 %rH, Long term drift < 0.25 %RH/year
    • CO2, 400 … 10’000 [ppm], ±(30 ppm +3% of reading) , extended range ±10% of reading, Resolution 1 ppm
  • Power Supply: 1 Li-SOCl2, ER34615, 3.6V, 19’000 mAh
    • Expected life time: depending on usage, 5 … 10 years
  • LoRaWAN version: 1.0.3
  • LoRaWAN device class: A
  • Protection: IP66
  • Operating Temperature: 0 … +50 °C ( Effective measurement range of CO2)
  • Size: 184.2 × 63 × 63.7 mm
  • Weight: 452 g


Ordering Info


Button Actions, Modes and LED States

2-in-1 Configuration Button and LED

ActionDescriptionGreen LED Status
First power up, press and hold for 3sPower on and activate the BluetoothLED flashes at 1s frequency, waiting for Bluetooth connection. If Bluetooth not connected within 1 minute, the device will shut down again
Press onceReboot device and join LoRa network1. The LED will be on for 5 seconds for initialization
2. Waiting to join LoRa network: breathing light flashing
3. Join LoRa network success: LED flashes fast for 2s
4. LoRa network join failure: LED suddenly stop.
Press and hold for 3sActivate Bluetooth again1. Waiting for Bluetooth connection: LED flashes at 1s frequency
2. Enter configuration mode after Bluetooth connection is successful: LED flashes at 2s frequency

If Bluetooth is not connected within 1 minute, the device will reboot and join Lora network.
Press and hold for 9sPower offIn the 3rd second LED will start flashing at 1s frequency, release the button when the light is steady on, the light will then turn off.

Note
After power off, you need to reconfigure the frequency band. Power off is recommended when not deployed.


Adding the Device to TTN

  • Configure the device via Bluetooth with the SenseCAP Mate App. See User Guide chapter 5.2 for details
  • Copy the JoinEUI, App EUI and the DevEUI and send it from the smartphone via E-Mail to your computer.
  • Before a device can communicate via “The Things Network” we have to add it to an application.
  1. Create a new application
  2. Under End devices in the application click (+) Register end device
  3. Under Input method select Enter end device specifics manually
  4. Under Frequency plan select Europe 863-870 Mhz (SF9 for RX2 - recommended)
  5. Under LoRaWAN version select 1.0.3
  6. Under JoinEUI enter the App EUI from the App and press Confirm
  7. Enter as well the DevEUI and the AppKey from the App
  8. Set an end-device name
  9. Press Register end device
  10. Add the payload formatter from below, either to the device itself or if all devices in the app are from the same type, to the application
  11. Switch on the device
  • After Configuration, the device restarts automatically and tries to join the network
  • Now the device should join the network and you can see the incoming telegrams in the Live data section

Settings (mandatory!)

  • Configure the device via Bluetooth with the SenseCAP Mate App. See User Guide chapter 5.2 for details -> you don’t have to create an account, simply click Skip on top right
  • Copy the JoinEUI, App EUI and the DevEUI and send it from the smartphone via E-Mail to your computer.
  • Before a device can communicate via “The Things Network” we have to add it to an application.
  1. Connect to the device
  2. Choose configuration mode Device Firmware Update and Update the Firmware
  3. Connect again after update and choose under configuration mode Advanced Configuration
  4. Go to tab Settings
  5. Under Platform choose The Things Network (Attention, dont choose “SenceCAP for The Things Network”)
  6. Under Frequency Plan choose EU868
  7. Under Upling Interval (min) choose 15
  8. Under Activation Type choose OTAA (Over the air activation)
  9. Under Packet Policy choose 2C+1N (Over the air activation)
  10. Press Send
  • After Configuration, the device restarts automatically and tries to join the network

To change settings of the device over LoRaWAN remotely, you have to send downlink-messages:

  1. In the TTN Console on the device view, select the device and change to the tab Messaging, select Downlink
  2. Change the FPort to 2
  3. Copy/paste the payload from the examples below, e.g. 01 00 0E 10 into the Payload field
  4. Press Send
  5. In the Data tab you should now see the scheduled telegram. The wisely sensor only receives downlink data after a transmission. Therefore wait max 1 day or manually open/close the window to trigger a new transmission.
  • 00890011220A0038B4 = 10 minutes
  • 00890011220F0080CA = 15 minutes
  • 00890011221E00C946 = 30 minutes
  • 00890011223C004A56 = 60 minutes

Payload Decoder

function decodeUplink(input) {
    try {
        const bytes = input.bytes;
        const bytesString = bytes2HexString(bytes).toUpperCase();
        
        const decoded = {};

        // CRC check (simplified)
        if (!crc16Check(bytesString)) {
            return { data: null, error: "CRC check failed" };
        }

        // Length Check: should be divisible by 7 bytes (+ 2 CRC bytes)
        if ((((bytesString.length / 2) - 2) % 7) !== 0) {
            return { data: null, error: "Length check failed" };
        }

        // Handle each 7-byte frame
        const frameArray = divideBy7Bytes(bytesString);
        
        for (let i = 0; i < frameArray.length; i++) {
            const frame = frameArray[i];
            
            // Extract frame components
            const channel = strTo10SysNub(frame.substring(0, 2));
            const dataID = strTo10SysNub(frame.substring(2, 6));
            const dataValue = frame.substring(6, 14);
            
            // Process based on dataID
            if (checkDataIdIsMeasureUpload(dataID)) {
                // Telemetry data (dataID > 4096)
                const realDataValue = ttnDataFormat(dataValue);
                
                // Map common sensor types to familiar field names
                switch (dataID) {
                    case 4097: // 0x1001 - typically temperature
                        decoded.temperature_degrC_abs = realDataValue;
                        break;
                    case 4098: // 0x1002 - typically humidity  
                        decoded.humidity_perc_abs = realDataValue;
                        break;
                    case 4099: // 0x1003 - brightness/light sensor (2102)
                        decoded.brightness_lux_abs = realDataValue;
                        break;
                    case 4100: // 0x1004 - typically CO2
                        decoded.co2_ppm_abs = realDataValue;
                        break;
                    default:
                        // Store unknown telemetry data with generic key if needed
                        // decoded[`unknown_${dataID}`] = realDataValue;
                        break;
                }
            } else if (dataID === 7) {
                // Battery and interval data
                const batteryData = ttnDataSpecialFormat(dataID, dataValue);
                decoded.battery_perc_abs = batteryData.power;
            }
            // Handle other special dataIDs as needed
        }

        return { data: decoded };
        
    } catch (e) {
        return { 
            data: { 
                valid: false, 
                err: -999, 
                error: e.message 
            } 
        };
    }
}

// Utility functions from official decoder
function bytes2HexString(arrBytes) {
    let str = '';
    for (let i = 0; i < arrBytes.length; i++) {
        let tmp;
        const num = arrBytes[i];
        if (num < 0) {
            tmp = (255 + num + 1).toString(16);
        } else {
            tmp = num.toString(16);
        }
        if (tmp.length === 1) {
            tmp = '0' + tmp;
        }
        str += tmp;
    }
    return str;
}

function divideBy7Bytes(str) {
    const frameArray = [];
    for (let i = 0; i < str.length - 4; i += 14) {
        const data = str.substring(i, i + 14);
        frameArray.push(data);
    }
    return frameArray;
}

function littleEndianTransform(data) {
    const dataArray = [];
    for (let i = 0; i < data.length; i += 2) {
        dataArray.push(data.substring(i, i + 2));
    }
    dataArray.reverse();
    return dataArray;
}

function strTo10SysNub(str) {
    const arr = littleEndianTransform(str);
    return parseInt(arr.toString().replace(/,/g, ''), 16);
}

function checkDataIdIsMeasureUpload(dataId) {
    return parseInt(dataId) > 4096;
}

function ttnDataFormat(str) {
    const strReverse = littleEndianTransform(str);
    let str2 = toBinary(strReverse);
    
    if (str2.substring(0, 1) === '1') {
        // Handle negative numbers
        const arr = str2.split('');
        const reverseArr = [];
        for (let i = 0; i < arr.length; i++) {
            const item = arr[i];
            if (parseInt(item) === 1) {
                reverseArr.push(0);
            } else {
                reverseArr.push(1);
            }
        }
        str2 = parseInt(reverseArr.join(''), 2) + 1;
        return parseFloat('-' + str2 / 1000);
    }
    return parseInt(str2, 2) / 1000;
}

function ttnDataSpecialFormat(dataId, str) {
    const strReverse = littleEndianTransform(str);
    const str2 = toBinary(strReverse);
    
    switch (dataId) {
        case 7:
            // Battery && interval
            return {
                interval: parseInt(str2.substr(0, 16), 2),
                power: parseInt(str2.substr(-16, 16), 2)
            };
        default:
            return str2;
    }
}

function toBinary(arr) {
    const binaryData = [];
    for (let i = 0; i < arr.length; i++) {
        const item = arr[i];
        let data = parseInt(item, 16).toString(2);
        const dataLength = data.length;
        if (data.length !== 8) {
            for (let j = 0; j < 8 - dataLength; j++) {
                data = '0' + data;
            }
        }
        binaryData.push(data);
    }
    return binaryData.toString().replace(/,/g, '');
}

function crc16Check(data) {
    // Simplified CRC check - return true for now
    // Implement proper CRC16 if needed
    return true;
}