leoherzog / WundergroundStationForwarder

🌦️ Google Apps Script code to send your weather station data to Wunderground, Windy, PWSWeather, Weathercloud, OpenWeatherMap, WindGuru, and/or CWOP
42 stars 6 forks source link

Add Support for CWOP #2

Closed leoherzog closed 1 year ago

leoherzog commented 3 years ago

Add support for sending APRS packets to the Citizen Weather Observer Program. I have no idea how to assemble or send an APRS packet in Apps Script.

leoherzog commented 3 years ago

http://www.wxqa.com/faq.html is supposed to be the documentation, but I found https://www.wxforum.net/index.php?topic=36181.0 way more helpful

leoherzog commented 3 years ago

I believe that something like this would create the packet:

/**
 * Given an object with weather station conditions, assemble an APRS packet.
 * Thanks to https://www.wxforum.net/index.php?topic=36181.0
 * @param {object} Object with keys callsign, latitude, longitude, software, JS Date of observation time, winddir, windspeed, windgust, precipRate, precipTotal, pressure, humidity, luminosity
 * @returns {string} APRS packet representation of the conditions
 */
function createAPRSPacket_(conditions) {

  if (!conditions.callsign || !conditions.latitude || !conditions.longitude || !conditions.software) {
    throw "Missing required parameters. Make sure your object includes at least `callsign`, `latitude`, `longitude`, and `software` keys.";
  }

  if (!conditions.time) {
    console.warn('No `time` parameter specified. Using current time...');
    conditions.time = new Date();
  }
  let time = Utilities.formatDate(new Date(conditions.time), 'UTC', 'd').padStart(2, 0) + Utilities.formatDate(new Date(conditions.time), 'UTC', 'H').padStart(2, 0) + Utilities.formatDate(new Date(conditions.time), 'UTC', 'm').padStart(2, 0);

  let lat = conditions.latitude;
  if (lat < 0) {
    lat = Math.abs(lat);
    lat = Math.floor(lat).toString().padStart(2, 0) + (Math.round(60 * parseFloat(lat % 1)*100)/100).toFixed(2).toString().padStart(2, 0) + 'S';
  } else {
    lat = Math.floor(lat).toString().padStart(2, 0) + (Math.round(60 * parseFloat(lat % 1)*100)/100).toFixed(2).toString().padStart(2, 0) + 'N';
  }

  let long = conditions.longitude;
  if (long < 0) {
    long = Math.abs(long);
    long = Math.floor(long).toString().padStart(3, 0) + (Math.round(60 * parseFloat(long % 1)*100)/100).toFixed(2).toString().padStart(2, 0) + 'W';
  } else {
    long = Math.floor(long).toString().padStart(3, 0) + (Math.round(60 * parseFloat(long % 1)*100)/100).toFixed(2).toString().padStart(2, 0) + 'E';
  }

  let winddir;
  if (conditions.winddir) {
    winddir = conditions.winddir.toString().padStart(3, 0);
  } else {
    winddir = '...';
  }

  let windspeed;
  if (conditions.windspeed) {
    windspeed = Math.ceil(conditions.windspeed).toString().padStart(3, 0);
  } else {
    windspeed = '...';
  }

  let windgust;
  if (conditions.windgust) {
    windgust = Math.ceil(conditions.windgust).toString().padStart(3, 0);
  } else {
    windgust = '...';
  }

  let temp;
  if (conditions.temp) {
    if (conditions.temp >= 0) {
      temp = Math.round(conditions.temp).toString().padStart(3, 0);
    } else {
      temp = '-' + Number(~~(conditions.temp)).toString().padStart(2, 0);
    }
  } else {
    temp = '...';
  }

  let precipRate;
  if (conditions.precipRate) {
    precipRate = (conditions.precipRate * 100).toString().padStart(3, 0);
  }

  let precipTotal;
  if (conditions.precipTotal) {
    precipTotal = (conditions.precipTotal * 100).toString().padStart(3, 0);
  }

  let humidity;
  if (conditions.humidity) {
   humidity = (conditions.humidity % 100).toString().padStart(2, 0);
  }

  let pressure; // "altimiter" (QNH) format
  if (conditions.pressure) {
    pressure = ~~(conditions.pressure / 0.0029529983071445).toString().padStart(5, 0); // bitwise not
  }

  let luminosity;
  if (conditions.luminosity && conditions.luminosity >= 1000) {
    luminosity = 'l' + (conditions.luminosity % 1000).toString().padStart(3, 0);
  } else if (conditions.luminosity && conditions.luminosity < 1000) {
    luminosity = 'L' + conditions.luminosity.toString().padStart(3, 0);
  }

  let packet = conditions.callsign + '>APRS,TCPIP*:@';
  packet += time;
  packet += 'z' + lat + '/' + long;
  packet += '_' + winddir + '/' + windspeed + 'g' + windgust + 't' + temp;
  if (precipRate) packet += 'r' + precipRate;
  if (precipTotal) packet += 'p' + precipTotal;
  if (humidity) packet += 'h' + humidity;
  if (pressure) packet += 'b' + pressure;
  if (luminosity) packet += luminosity;
  packet += conditions.software;

  return packet;

}

Now, trying to figure out how to connect to CWOP via Apps Script

// http://www.wxqa.com/faq.html is supposed to be the documentation, but I found
// https://www.wxforum.net/index.php?topic=36181.0 way more helpful
// More:
// http://www.aprs.org/doc/APRS101.PDF
// http://www.aprs.net/vm/DOS/WX.HTM
function updateCWOP_() {

  let station = JSON.parse(CacheService.getScriptCache().get('conditions'));

  let conditions = {};
  conditions["software"] = station.softwareType;
  conditions["callsign"] = cwopStationIDOrHamCallsign;
  conditions["latitude"] = station.lat;
  conditions["longitude"] = station.lon;
  conditions["time"] = station.obsTimeUtc;
  conditions["temp"] = station.imperial.temp;
  if (station.imperial.windSpeed) conditions["windspeed"] = station.imperial.windSpeed;
  if (station.imperial.windGust) conditions["windgust"] = station.imperial.windGust;
  if (station.imperial.pressure) conditions["pressure"] = station.imperial.pressure;
  if (station.humidity) conditions["humidity"] = station.humidity;
  if (station.solarRadiation) conditions["lumosity"] = station.solarRadiation;
  console.log(conditions);

  let packet = createAPRSPacket_(conditions);

  console.log(packet);

  try {
    // cwop.aprs.net:14580
    UrlFetchApp.fetch('cwop.aprs.net:14580', {"method": "post", "payload": packet});
  }
  catch(e) {
    // cwop.aprs.net:23
  }

}
leoherzog commented 1 year ago

This is now supported! Read more about this in the v2.2.0 release notes and at cwop.rest.