diff --git a/vendor/corssystem/corssystem.png b/vendor/corssystem/corssystem.png new file mode 100644 index 0000000000..5f9d36bb8c Binary files /dev/null and b/vendor/corssystem/corssystem.png differ diff --git a/vendor/corssystem/cortu-gen-2.yaml b/vendor/corssystem/cortu-gen-2.yaml new file mode 100644 index 0000000000..6c0ed71b9a --- /dev/null +++ b/vendor/corssystem/cortu-gen-2.yaml @@ -0,0 +1,129 @@ +name: CorTU Genesis 2 # Device name can not contain the vendor name +description: Remote Data Acquisition Board with Support for LoRaWAN and RF + +# Hardware versions (optional, use when you have revisions) +hardwareVersions: + - version: '1.0' + numeric: 1 + - version: '1.0-rev-A' + numeric: 2 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '1.0' + numeric: 1 + # Corresponding hardware versions (optional) + hardwareVersions: + - '1.0' + + # Firmware features (optional) + # Valid values are: remote rejoin (trigger a join from the application layer), transmission interval (configure how + # often he device sends a message). + features: + - remote rejoin + - transmission interval + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, + # RU864-870 + profiles: + EU863-870: + # Optional identifier of the vendor of the profile. When you specify the vendorID, the profile is loaded from + # the vendorID's folder. This allows you to reuse profiles from module or LoRaWAN end device stack vendors. + # If vendorID is empty, the current vendor ID is used. In this example, the vendorID is the current vendor ID, + # which is verbose. + # vendorID: example + # Identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters) + id: cortugenesis-profile + # lorawanCertified: true + codec: cortugenesis-codec + US902-928: + id: cortugenesis-profile + # lorawanCertified: true + codec: cortugenesis-codec + +# Type of device (optional) +# Valid values are: devkit, module, cots +deviceType: cots + +# Sensors that this device features (optional) +# Valid values are: +# 4-20 ma, accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co, co2, conductivity, current, digital input, +# digital output, dissolved oxygen, distance, dust, energy, gps, gyroscope, h2s, hall effect, humidity, iaq, infrared, leaf wetness, level, +# light, lightning, link, magnetometer, moisture, motion, nfc, no, no2, o3, occupancy, optical meter, particulate matter, ph, pir, +# pm2.5, pm10, potentiometer, power, precipitation, pressure, proximity, pulse count, pulse frequency, radar, rainfall, reed switch, rssi, +# sap flow, smart valve, smoke, snr, so2, solar radiation, sound, strain, surface temperature, switch, temperature, tilt, time, turbidity, +# tvoc, uv, vapor pressure, velocity, vibration, voltage, water potential, water, weight, wifi ssid, wind direction, wind speed. +sensors: + - 4-20 ma + - analog input + - digital input + +# Additional radios that this device has (optional) +# Valid values are: ble, nfc, wifi, cellular. +# additionalRadios: +# - ble +# - cellular + +# Bridge interfaces (optional) +# Valid values are: modbus, m-bus, can bus, rs-485, sdi-12, analog, ethernet. +bridgeInterfaces: + - modbus + - rs-485 + - analog + +# Dimensions in mm (optional) +# Use width, height, length and/or diameter +dimensions: + width: 110 + length: 110 + height: 40 + +# Weight in grams (optional) +weight: 350 + +# Operating conditions (optional) +operatingConditions: + # Temperature (Celsius) + temperature: + min: -30 + max: 85 + # Relative humidity (fraction of 1) + relativeHumidity: + min: 0 + max: 0.97 + +# IP rating (optional) +# ipCode: IP64 + +# Key provisioning (optional) +# Valid values are: custom (user can configure keys), join server and manifest. +keyProvisioning: + - custom + - join server + +# Key programming (optional) +# Valid values are: bluetooth, nfc, wifi, ethernet (via a webpage), serial (when the user has a serial interface to set the keys) +# and firmware (when the user should change the firmware to set the keys). +keyProgramming: + - serial + - firmware + +# Key security (optional) +# Valid values are: none, read protected and secure element. +keySecurity: none + +# Firmware programming (optional) +# Valid values are: serial (when the user has a serial interface to update the firmware), ethernet, fuota lorawan (when the device +# supports LoRaWAN FUOTA via standard interfaces) and fuota other (other wireless update mechanism). +firmwareProgramming: + - serial + # - fuota lorawan + +# Product and data sheet URLs (optional) +productURL: https://corssystem.com/products/cortu-v1-0/ + +# Photos +photos: + main: cortugenesis.png # Image needs to have a transparent background diff --git a/vendor/corssystem/cortugenesis-codec.yaml b/vendor/corssystem/cortugenesis-codec.yaml new file mode 100644 index 0000000000..9ad4591346 --- /dev/null +++ b/vendor/corssystem/cortugenesis-codec.yaml @@ -0,0 +1,3 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +uplinkDecoder: + fileName: cortugenesis.js diff --git a/vendor/corssystem/cortugenesis-profile.yaml b/vendor/corssystem/cortugenesis-profile.yaml new file mode 100644 index 0000000000..844110d4b0 --- /dev/null +++ b/vendor/corssystem/cortugenesis-profile.yaml @@ -0,0 +1,17 @@ +# LoRaWAN MAC version: 1.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4 or 1.1 +macVersion: '1.0.3' +regionalParametersVersion: 'RP001-1.0.3-RevA' + +# Whether the end device supports join (OTAA) or not (ABP) +supportsJoin: true + +# Maximum EIRP +maxEIRP: 16 +# Whether the end device supports 32-bit frame counters +supports32bitFCnt: false + +# Whether the end device supports class B +supportsClassB: false + +# Whether the end device supports class C +supportsClassC: false diff --git a/vendor/corssystem/cortugenesis.js b/vendor/corssystem/cortugenesis.js new file mode 100644 index 0000000000..46decfcdc1 --- /dev/null +++ b/vendor/corssystem/cortugenesis.js @@ -0,0 +1,174 @@ + +//CorTU Payload Formatter +function parseIncomingData(data) { + const delimiter = "\n"; + // No need to use TextDecoder as data is already a string + const dataSets = data.split(delimiter).filter(Boolean); + return dataSets.map(dataSet => { + const [huffmanCodesStr, encodedData] = dataSet.split("|"); + const huffmanCodesArray = huffmanCodesStr.trim().split(';'); // Assuming ';' separates the codes + const huffmanCodes = {}; + for (let i = 0; i <(huffmanCodesArray.length)-1; i++) { + const [key, value] = huffmanCodesArray[i].split(':'); + huffmanCodes[key] = value; + } + return { + huffmanCodes, + encodedData + }; + }); +} +// Function to decode a Huffman-encoded bit stream using the provided codes +function decodeHuffman(encodedData, huffmanCodes) { + let decodedMessage = []; + let currentCode = ""; + // Iterate over each bit in the encoded data + for (let bit of encodedData) { + currentCode += bit; + // Check if the current code matches any entry in the Huffman codes + if (currentCode in huffmanCodes) { + // Append the corresponding character to the decoded message + decodedMessage.push(huffmanCodes[currentCode]); + // Reset the current code + currentCode = ""; + } + } + // Return the decoded message as a string + return decodedMessage.join(''); +} +function lppDecode(bytes) { + var sensor_types = { + 0 : {'size': 1, 'name': 'digital_in', 'signed': false, 'divisor': 1}, + 1 : {'size': 1, 'name': 'digital_out', 'signed': false, 'divisor': 1}, + 2 : {'size': 2, 'name': 'analog_in', 'signed': true , 'divisor': 100}, + 3 : {'size': 2, 'name': 'analog_out', 'signed': true , 'divisor': 100}, + 100: {'size': 4, 'name': 'generic', 'signed': false, 'divisor': 1}, + 101: {'size': 2, 'name': 'illuminance', 'signed': false, 'divisor': 1}, + 102: {'size': 1, 'name': 'presence', 'signed': false, 'divisor': 1}, + 103: {'size': 2, 'name': 'temperature', 'signed': true , 'divisor': 10}, + 104: {'size': 1, 'name': 'humidity', 'signed': false, 'divisor': 2}, + 113: {'size': 6, 'name': 'accelerometer', 'signed': true , 'divisor': 1000}, + 115: {'size': 2, 'name': 'barometer', 'signed': false, 'divisor': 10}, + 116: {'size': 2, 'name': 'voltage', 'signed': false, 'divisor': 100}, + 117: {'size': 2, 'name': 'current', 'signed': false, 'divisor': 1000}, + 118: {'size': 4, 'name': 'frequency', 'signed': false, 'divisor': 1}, + 120: {'size': 1, 'name': 'percentage', 'signed': false, 'divisor': 1}, + 121: {'size': 2, 'name': 'altitude', 'signed': true, 'divisor': 1}, + 125: {'size': 2, 'name': 'concentration', 'signed': false, 'divisor': 1}, + 128: {'size': 2, 'name': 'power', 'signed': false, 'divisor': 1}, + 130: {'size': 4, 'name': 'distance', 'signed': false, 'divisor': 1000}, + 131: {'size': 4, 'name': 'energy', 'signed': false, 'divisor': 1000}, + 132: {'size': 2, 'name': 'direction', 'signed': false, 'divisor': 1}, + 133: {'size': 4, 'name': 'time', 'signed': false, 'divisor': 1}, + 134: {'size': 6, 'name': 'gyrometer', 'signed': true , 'divisor': 100}, + 135: {'size': 3, 'name': 'colour', 'signed': false, 'divisor': 1}, + 136: {'size': 9, 'name': 'gps', 'signed': true, 'divisor': [10000,10000,100]}, + 142: {'size': 1, 'name': 'switch', 'signed': false, 'divisor': 1}, + }; + function arrayToDecimal(stream, is_signed, divisor) { + var value = 0; + for (var i = 0; i < stream.length; i++) { + if (stream[i] > 0xFF) + throw 'Byte value overflow!'; + value = (value << 8) | stream[i]; + } + if (is_signed) { + var edge = 1 << (stream.length) * 8; // 0x1000.. + var max = (edge - 1) >> 1; // 0x0FFF.. >> 1 + value = (value > max) ? value - edge : value; + } + value /= divisor; + return value; + } + var sensors = []; + var i = 0; + while (i < bytes.length) { + var s_no = bytes[i++]; + var s_type = bytes[i++]; + if (typeof sensor_types[s_type] == 'undefined') { + throw 'Sensor type error!: ' + s_type; + } + var s_value = 0; + var type = sensor_types[s_type]; + switch (s_type) { + case 113: // Accelerometer + case 134: // Gyrometer + s_value = { + 'x': arrayToDecimal(bytes.slice(i+0, i+2), type.signed, type.divisor), + 'y': arrayToDecimal(bytes.slice(i+2, i+4), type.signed, type.divisor), + 'z': arrayToDecimal(bytes.slice(i+4, i+6), type.signed, type.divisor) + }; + break; + case 136: // GPS Location + s_value = { + 'latitude': arrayToDecimal(bytes.slice(i+0, i+3), type.signed, type.divisor[0]), + 'longitude': arrayToDecimal(bytes.slice(i+3, i+6), type.signed, type.divisor[1]), + 'altitude': arrayToDecimal(bytes.slice(i+6, i+9), type.signed, type.divisor[2]) + }; + break; + case 135: // Colour + s_value = { + 'r': arrayToDecimal(bytes.slice(i+0, i+1), type.signed, type.divisor), + 'g': arrayToDecimal(bytes.slice(i+1, i+2), type.signed, type.divisor), + 'b': arrayToDecimal(bytes.slice(i+2, i+3), type.signed, type.divisor) + }; + break; + default: // All the rest + s_value = arrayToDecimal(bytes.slice(i, i + type.size), type.signed, type.divisor); + break; + } + sensors.push({ + 'channel': s_no, + 'type': s_type, + 'name': type.name, + 'value': s_value + }); + i += type.size; + } + return sensors; +} +// Convert hex string to byte array +function hexStringToByteArray(hexString) { +var bytes = []; +for (var i = 0; i < hexString.length; i += 2) { + bytes.push(parseInt(hexString.substr(i, 2), 16)); +} +return bytes; +} +function decodeUplink(input) +{ +// Convert decimal bytes to ASCII string +var dataString = ''; +for (var i = 0; i < input.bytes.length; i++) { + dataString += String.fromCharCode(input.bytes[i]); +} +//Separate the Huffman codes from the encoded data +let dataset = parseIncomingData(dataString); +// Initialize an object to store all Huffman codes +let allHuffmanCodes = {}; +// Merge all Huffman codes into one object +dataset.forEach(item => { +Object.assign(allHuffmanCodes, item.huffmanCodes); +}); +//String to store encoded (compressed) data +let allencodedData = dataset.map(item => item.encodedData).join(''); +//Decoding the data +let decodedData = decodeHuffman(allencodedData, allHuffmanCodes); +//Converting string data output from Huffman encode to hex byte array hex +let bytes = hexStringToByteArray(decodedData); +// flat output (like original Cayenne lpp decoder): +var response = {}; +lppDecode(bytes, 1).forEach(function(field) { + response[field.name + '_' + field.channel] = field.value; + }); +return { + data: { + bytes: input.bytes, + decoded: { + message: response + } + }, + warnings: [], + errors: [] +}; +} \ No newline at end of file diff --git a/vendor/corssystem/cortugenesis.png b/vendor/corssystem/cortugenesis.png new file mode 100644 index 0000000000..dce0b8d468 Binary files /dev/null and b/vendor/corssystem/cortugenesis.png differ diff --git a/vendor/corssystem/index.yaml b/vendor/corssystem/index.yaml new file mode 100644 index 0000000000..0a6cfca680 --- /dev/null +++ b/vendor/corssystem/index.yaml @@ -0,0 +1,2 @@ +endDevices: + - cortu-gen-2 diff --git a/vendor/index.yaml b/vendor/index.yaml index 2598c8c10f..343b007c71 100644 --- a/vendor/index.yaml +++ b/vendor/index.yaml @@ -2070,6 +2070,15 @@ vendors: linkedin: https://www.linkedin.com/company/cicicom-ltd facebook: https://www.facebook.com/cicicom + - id: corssystem + name: Cors System Technologies + description: Cors System Technologies is a Nigerian digital transformation company, with a focus in bringing existing industries into Industry 4.0. + logo: corssystem.png + website: https://corssystem.com/ + social: + linkedin: https://www.linkedin.com/company/cors-system/ + github: corssystem + - id: atomsenses name: Atomsenses description: Atomsenses is a specialist IoT solution provider focusing on Lorawan sensors for indoor air quality monitoring, our vision is to transform how we manage and maintain healthy indoor environments.