Okay, I figured out the TTN decoding bit. Now TTN is decoding properly. I guess I now have to figure out how to update the Datacake version.
Well ChatGPT just helped me build the required decoder code for Datacake. The last time I wrote code was C++ and Fortran over 30 years ago. Shared the old Datacake code and provided it with the The Things Stack payload formatter code as well as the decoded output format and then kept feeding it the errors until it was working!!
Wow!! Very impressive. Would you mind sharing the working Datacake decoder?
Hi there,
Good Old Fortran77 It hasn’t change … MUCH
Got to love you some ChatGPT…
Do please post the code, Use the code tags above"</>" paste it in there, so others may benefit! and Thanks for contributing.
GL PJ
[quote=“PJ_Glasso, post:24, topic:283379”]
</>
// Mapping measurement IDs to human-readable field names
var measurementIdToFieldName = {
‘4097’: ‘AIR_TEMPERATURE’,
‘4098’: ‘AIR_HUMIDITY’,
‘4099’: ‘LIGHT_INTENSITY’,
‘4190’: ‘UV_INDEX’,
‘4105’: ‘WIND_SPEED’,
‘4104’: ‘WIND_DIRECTION’,
‘4113’: ‘RAIN_GAUGE’,
‘4101’: ‘BAROMETRIC_PRESSURE’,
‘4191’: ‘PEAK_WIND_GUST’,
‘4213’: ‘RAIN_ACCUMULATION’,
‘4120’: ‘BATTERY’,
};
// Function to decode the SenseCAP payload and extract the data
function decodeSensecapPayload(data) {
if (!data || data.length === 0) {
console.log(“Input data is empty or undefined.”);
return { err: 1, messages: [], valid: false };
}
// Convert byte array to hex string
data = data2HexString(data).toUpperCase();
console.log("Payload in hex format: " + data);
var result = {
err: 0,
payload: data,
valid: true,
messages: [],
};
var splitArray = dataSplit(data);
console.log("Split data array: " + JSON.stringify(splitArray));
var decoderArray = [];
for (var i = 0; i < splitArray.length; i++) {
var item = splitArray[i];
var dataId = item.dataId;
var dataValue = item.dataValue;
console.log("Processing dataId: " + dataId + " with dataValue: " + dataValue);
var messages = dataIdAndDataValueJudge(dataId, dataValue);
console.log("Decoded messages: " + JSON.stringify(messages));
if (messages.length > 0) {
decoderArray.push(messages);
}
}
result.messages = decoderArray;
return result;
}
// Function to split the incoming data into manageable pieces
function dataSplit(data) {
var frameArray = [];
console.log("Splitting data: " + data);
if (!data || data.length === 0) {
console.log(“No data to split.”);
return frameArray;
}
while (data.length > 2) {
var dataId = data.substring(0, 2);
var dataValue = ‘’;
var dataObj = {};
console.log("Processing dataId: " + dataId);
switch (dataId) {
case '01':
case '4A':
dataValue = data.substring(2, 22);
data = data.substring(22);
break;
case '02':
case '4B':
dataValue = data.substring(2, 18);
data = data.substring(18);
break;
case '03':
dataValue = data.substring(2, 4);
data = data.substring(4);
break;
case '4C':
dataValue = data.substring(2, 14);
data = data.substring(14);
break;
default:
console.log("Unknown dataId: " + dataId);
dataValue = '';
data = '';
break;
}
if (dataValue) {
dataObj = {
dataId: dataId,
dataValue: dataValue,
};
frameArray.push(dataObj);
}
}
console.log("Frame array after splitting: " + JSON.stringify(frameArray));
return frameArray;
}
// Function to match dataId and dataValue to their corresponding measurements
function dataIdAndDataValueJudge(dataId, dataValue) {
var messages = [];
switch (dataId) {
case ‘01’:
case ‘4A’:
var temperature = loraWANV2DataFormat(dataValue.substring(0, 4), 10);
var humidity = loraWANV2DataFormat(dataValue.substring(4, 6));
var illumination = loraWANV2DataFormat(dataValue.substring(6, 14));
var uvIndex = loraWANV2DataFormat(dataValue.substring(14, 16), 10);
var windSpeed = loraWANV2DataFormat(dataValue.substring(16, 20), 10);
messages.push({
measurementValue: temperature,
measurementId: '4097',
type: 'Air Temperature',
});
messages.push({
measurementValue: humidity,
measurementId: '4098',
type: 'Air Humidity',
});
messages.push({
measurementValue: illumination,
measurementId: '4099',
type: 'Light Intensity',
});
messages.push({
measurementValue: uvIndex,
measurementId: '4190',
type: 'UV Index',
});
messages.push({
measurementValue: windSpeed,
measurementId: '4105',
type: 'Wind Speed',
});
break;
case '02':
case '4B':
var windDirection = loraWANV2DataFormat(dataValue.substring(0, 4));
var rainfall = loraWANV2DataFormat(dataValue.substring(4, 12), 1000);
var airPressure = loraWANV2DataFormat(dataValue.substring(12, 16), 0.1);
messages.push({
measurementValue: windDirection,
measurementId: '4104',
type: 'Wind Direction Sensor',
});
messages.push({
measurementValue: rainfall,
measurementId: '4113',
type: 'Rain Gauge',
});
messages.push({
measurementValue: airPressure,
measurementId: '4101',
type: 'Barometric Pressure',
});
break;
case '4C':
var peakWind = loraWANV2DataFormat(dataValue.substring(0, 4), 10);
var rainAccumulation = loraWANV2DataFormat(dataValue.substring(4, 12), 1000);
messages.push({
measurementValue: peakWind,
measurementId: '4191',
type: 'Peak Wind Gust',
});
messages.push({
measurementValue: rainAccumulation,
measurementId: '4213',
type: 'Rain Accumulation',
});
break;
case '03':
var battery = loraWANV2DataFormat(dataValue);
messages.push({
measurementValue: battery,
measurementId: '4120',
type: 'Battery',
});
break;
default:
console.log("Unrecognized dataId: " + dataId);
break;
}
return messages;
}
// Helper functions for data formatting
function loraWANV2DataFormat(str, divisor) {
divisor = divisor || 1;
var strReverse = bigEndianTransform(str);
var str2 = toBinary(strReverse);
if (str2[0] === ‘1’) {
var arr = str2.split(’’).map(function (bit) {
return bit === ‘1’ ? ‘0’ : ‘1’;
});
str2 = (parseInt(arr.join(’’), 2) + 1).toString();
return parseFloat(’-’ + str2 / divisor);
}
return parseInt(str2, 2) / divisor;
}
function bigEndianTransform(data) {
var arr = [];
for (var i = 0; i < data.length; i += 2) {
arr.push(data.substring(i, i + 2));
}
return arr;
}
function toBinary(arr) {
return arr.map(function (byte) {
var binaryString = parseInt(byte, 16).toString(2);
while (binaryString.length < 8) {
binaryString = ‘0’ + binaryString;
}
return binaryString;
}).join(’’);
}
function data2HexString(arrBytes) {
if (!arrBytes || arrBytes.length === 0) {
console.log(“No data in arrBytes to convert.”);
return “”;
}
var str = ‘’;
for (var i = 0; i < arrBytes.length; i++) {
var tmp;
var 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;
}
console.log("Hex string after conversion: " + str);
return str;
}
// Main Decoder function
function Decoder(bytes, port) {
console.log("Input bytes: " + bytes);
// Check if the input bytes are empty and use a test payload if they are
if (!bytes || bytes.length === 0) {
console.log(“Input bytes are empty. Using a test payload.”);
// Use a sample payload for testing
bytes = [0x4A, 0xFF, 0xE4, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4B, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x23, 0xDD, 0x4C, 0x00, 0x00, 0x00, 0x00];
console.log("Test payload: " + bytes);
}
// Proceed with decoding the payload
var decodedSensecapFields = decodeSensecapPayload(bytes).messages;
var datacakeFields = [];
if (decodedSensecapFields.length === 0) {
console.log(“No decoded fields available, returning empty array.”);
return datacakeFields;
}
for (var i = 0; i < decodedSensecapFields.length; i++) {
var arrayEntry = decodedSensecapFields[i];
arrayEntry.forEach(function (entry) {
// Map measurementId to a human-readable field name
var fieldName = measurementIdToFieldName[entry.measurementId] || ‘MEASUREMENT_ID_’ + entry.measurementId;
datacakeFields.push({
field: fieldName,
value: entry.measurementValue,
});
});
}
console.log("Decoded fields to return to Datacake: " + JSON.stringify(datacakeFields));
return datacakeFields;
}
</>
Here is the Datacake dashboard: Loading...
Now I just need to go mount it on the pole to replace my old unit.!!
Hi there, Well almost.
// Mapping measurement IDs to human-readable field names
var measurementIdToFieldName = {
‘4097’: ‘AIR_TEMPERATURE’,
‘4098’: ‘AIR_HUMIDITY’,
‘4099’: ‘LIGHT_INTENSITY’,
‘4190’: ‘UV_INDEX’,
‘4105’: ‘WIND_SPEED’,
‘4104’: ‘WIND_DIRECTION’,
‘4113’: ‘RAIN_GAUGE’,
‘4101’: ‘BAROMETRIC_PRESSURE’,
‘4191’: ‘PEAK_WIND_GUST’,
‘4213’: ‘RAIN_ACCUMULATION’,
‘4120’: ‘BATTERY’,
};
// Function to decode the SenseCAP payload and extract the data
function decodeSensecapPayload(data) {
if (!data || data.length === 0) {
console.log(“Input data is empty or undefined.”);
return { err: 1, messages: [], valid: false };
}
// Convert byte array to hex string
data = data2HexString(data).toUpperCase();
console.log("Payload in hex format: " + data);
var result = {
err: 0,
payload: data,
valid: true,
messages: [],
};
var splitArray = dataSplit(data);
console.log("Split data array: " + JSON.stringify(splitArray));
var decoderArray = [];
for (var i = 0; i < splitArray.length; i++) {
var item = splitArray[i];
var dataId = item.dataId;
var dataValue = item.dataValue;
console.log("Processing dataId: " + dataId + " with dataValue: " + dataValue);
var messages = dataIdAndDataValueJudge(dataId, dataValue);
console.log("Decoded messages: " + JSON.stringify(messages));
if (messages.length > 0) {
decoderArray.push(messages);
}
}
result.messages = decoderArray;
return result;
}
// Function to split the incoming data into manageable pieces
function dataSplit(data) {
var frameArray = [];
console.log("Splitting data: " + data);
if (!data || data.length === 0) {
console.log(“No data to split.”);
return frameArray;
}
while (data.length > 2) {
var dataId = data.substring(0, 2);
var dataValue = ‘’;
var dataObj = {};
console.log("Processing dataId: " + dataId);
switch (dataId) {
case '01':
case '4A':
dataValue = data.substring(2, 22);
data = data.substring(22);
break;
case '02':
case '4B':
dataValue = data.substring(2, 18);
data = data.substring(18);
break;
case '03':
dataValue = data.substring(2, 4);
data = data.substring(4);
break;
case '4C':
dataValue = data.substring(2, 14);
data = data.substring(14);
break;
default:
console.log("Unknown dataId: " + dataId);
dataValue = '';
data = '';
break;
}
if (dataValue) {
dataObj = {
dataId: dataId,
dataValue: dataValue,
};
frameArray.push(dataObj);
}
}
console.log("Frame array after splitting: " + JSON.stringify(frameArray));
return frameArray;
}
// Function to match dataId and dataValue to their corresponding measurements
function dataIdAndDataValueJudge(dataId, dataValue) {
var messages = [];
switch (dataId) {
case ‘01’:
case ‘4A’:
var temperature = loraWANV2DataFormat(dataValue.substring(0, 4), 10);
var humidity = loraWANV2DataFormat(dataValue.substring(4, 6));
var illumination = loraWANV2DataFormat(dataValue.substring(6, 14));
var uvIndex = loraWANV2DataFormat(dataValue.substring(14, 16), 10);
var windSpeed = loraWANV2DataFormat(dataValue.substring(16, 20), 10);
messages.push({
measurementValue: temperature,
measurementId: '4097',
type: 'Air Temperature',
});
messages.push({
measurementValue: humidity,
measurementId: '4098',
type: 'Air Humidity',
});
messages.push({
measurementValue: illumination,
measurementId: '4099',
type: 'Light Intensity',
});
messages.push({
measurementValue: uvIndex,
measurementId: '4190',
type: 'UV Index',
});
messages.push({
measurementValue: windSpeed,
measurementId: '4105',
type: 'Wind Speed',
});
break;
case '02':
case '4B':
var windDirection = loraWANV2DataFormat(dataValue.substring(0, 4));
var rainfall = loraWANV2DataFormat(dataValue.substring(4, 12), 1000);
var airPressure = loraWANV2DataFormat(dataValue.substring(12, 16), 0.1);
messages.push({
measurementValue: windDirection,
measurementId: '4104',
type: 'Wind Direction Sensor',
});
messages.push({
measurementValue: rainfall,
measurementId: '4113',
type: 'Rain Gauge',
});
messages.push({
measurementValue: airPressure,
measurementId: '4101',
type: 'Barometric Pressure',
});
break;
case '4C':
var peakWind = loraWANV2DataFormat(dataValue.substring(0, 4), 10);
var rainAccumulation = loraWANV2DataFormat(dataValue.substring(4, 12), 1000);
messages.push({
measurementValue: peakWind,
measurementId: '4191',
type: 'Peak Wind Gust',
});
messages.push({
measurementValue: rainAccumulation,
measurementId: '4213',
type: 'Rain Accumulation',
});
break;
case '03':
var battery = loraWANV2DataFormat(dataValue);
messages.push({
measurementValue: battery,
measurementId: '4120',
type: 'Battery',
});
break;
default:
console.log("Unrecognized dataId: " + dataId);
break;
}
return messages;
}
// Helper functions for data formatting
function loraWANV2DataFormat(str, divisor) {
divisor = divisor || 1;
var strReverse = bigEndianTransform(str);
var str2 = toBinary(strReverse);
if (str2[0] === ‘1’) {
var arr = str2.split(’’).map(function (bit) {
return bit === ‘1’ ? ‘0’ : ‘1’;
});
str2 = (parseInt(arr.join(’’), 2) + 1).toString();
return parseFloat(’-’ + str2 / divisor);
}
return parseInt(str2, 2) / divisor;
}
function bigEndianTransform(data) {
var arr = [];
for (var i = 0; i < data.length; i += 2) {
arr.push(data.substring(i, i + 2));
}
return arr;
}
function toBinary(arr) {
return arr.map(function (byte) {
var binaryString = parseInt(byte, 16).toString(2);
while (binaryString.length < 8) {
binaryString = ‘0’ + binaryString;
}
return binaryString;
}).join(’’);
}
function data2HexString(arrBytes) {
if (!arrBytes || arrBytes.length === 0) {
console.log(“No data in arrBytes to convert.”);
return “”;
}
var str = ‘’;
for (var i = 0; i < arrBytes.length; i++) {
var tmp;
var 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;
}
console.log("Hex string after conversion: " + str);
return str;
}
// Main Decoder function
function Decoder(bytes, port) {
console.log("Input bytes: " + bytes);
// Check if the input bytes are empty and use a test payload if they are
if (!bytes || bytes.length === 0) {
console.log(“Input bytes are empty. Using a test payload.”);
// Use a sample payload for testing
bytes = [0x4A, 0xFF, 0xE4, 0x5A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4B, 0x00, 0xD8, 0x00, 0x00, 0x00, 0x23, 0xDD, 0x4C, 0x00, 0x00, 0x00, 0x00];
console.log("Test payload: " + bytes);
}
// Proceed with decoding the payload
var decodedSensecapFields = decodeSensecapPayload(bytes).messages;
var datacakeFields = [];
if (decodedSensecapFields.length === 0) {
console.log(“No decoded fields available, returning empty array.”);
return datacakeFields;
}
for (var i = 0; i < decodedSensecapFields.length; i++) {
var arrayEntry = decodedSensecapFields[i];
arrayEntry.forEach(function (entry) {
// Map measurementId to a human-readable field name
var fieldName = measurementIdToFieldName[entry.measurementId] || ‘MEASUREMENT_ID_’ + entry.measurementId;
datacakeFields.push({
field: fieldName,
value: entry.measurementValue,
});
});
}
console.log("Decoded fields to return to Datacake: " + JSON.stringify(datacakeFields));
return datacakeFields;
}
WOW, That looks better
Cool,
GL PJ
in college at Virginia Tech… in 1993… we were using a book called Fortran77… and i always thought that was odd… considering i was born in 1974… What Da?
HI there,
LOL, I was using it in 87-89 on an IBM- AS400 or HP 3000 mainframes, It wasn’t cool until we got to the SUN Spark workstations though much later.
Everything was so BIG, Today everything is “XIAO” (Tiny)
GL PJ
i was in on the ground floor with Sun Workstations and ESRI GIS… i didn’t like or understand linux… it was all too cumbersome after coming from Apple IIE public eduction… As an engineer… we were always told GIS stood for Get It Surveyed"… In hindsight… i wish i learned more of that stuff… I was begging for a 20 Megabyte Hard Drive, now a 2 Tarabyte Drive cant hold all the Ortho and DEM data files
HI there,
Wow yea that survey data is a big lot. We used them for terrain mapping and navigation for weapon systems at the SEI in pittsburgh.
now it’s all on board. i.e. Ukrainians’ don’t need GPS anymore as a main NAV component.
LLM’s and maps and pictures wifi access points
amazing time for tech!
GL PJ
yep it is amazing what they can do with offline navigation now!