Skip to content

Instantly share code, notes, and snippets.

@tomfanning
Last active April 14, 2017 21:59
Show Gist options
  • Save tomfanning/a79a5de40bdc6913c1ca2bd35b1fb57a to your computer and use it in GitHub Desktop.
Save tomfanning/a79a5de40bdc6913c1ca2bd35b1fb57a to your computer and use it in GitHub Desktop.
Minimal C# .NET implementation of a position-reporting APRS client for a serial KISS TNC (tested with Mobilinkd)
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO.Ports;
using System.Text;
namespace kiss_test
{
class Program
{
const byte KISS_FEND = 0xc0;
const byte KISS_FESC = 0xdb;
const byte KISS_TFEND = 0xdc;
const byte KISS_TFESC = 0xdd;
const byte KISS_CMD_DATAFRAME0 = 0x00;
const byte KISS_CMD_TXDELAY = 0x01;
const byte KISS_CMD_TXTAIL = 0x04;
const byte KISS_CMD_SETHARDWARE = 0x06;
static void Main(string[] args)
{
double lat = 51.447;
double lon = -1.028;
string srcCall = "N0CALL";
if (srcCall == "N0CALL")
{
throw new Exception("Set callsign");
}
string infoField = BuildPositionReportWithoutTimestamp(lat, lon, Symbol.Car, "Arduino testing");
byte[] packetBytes = AssemblePacket(srcCall, "WIDE1-1", new[] { "WIDE2-1" }, infoField);
var kissFrame = new List<byte>();
kissFrame.Add(KISS_FEND);
kissFrame.Add(KISS_CMD_DATAFRAME0);
kissFrame.AddRange(packetBytes);
kissFrame.Add(KISS_FEND);
byte[] kissFrameBytes = kissFrame.ToArray();
foreach (byte b in kissFrameBytes)
{
Console.Write("0x{0:X2}, ", b);
}
using (var sp = new SerialPort("COM3", 38400, Parity.None, 8, StopBits.One))
{
sp.Open();
sp.Write(kissFrameBytes);
}
}
static string BuildPositionReportWithoutTimestamp(double lat, double lon, Symbol sym, string comment)
{
string latString = BuildLatString(lat);
string lonString = BuildLonString(lon);
var sc = SymbolTable[sym];
string result = $"!{latString}{sc.Table}{lonString}{sc.Code}{comment}";
return result;
}
/// <summary>
/// Fixed 9 character field.
/// In degrees and decimal minutes to two DP, followed by E or W.
/// e.g. 07201.75W = 72 degrees, 1.75 minutes (1m45s) west
/// </summary>
/// <param name="lon"></param>
/// <returns></returns>
static string BuildLonString(double lon)
{
double absLon = Math.Abs(lon);
string degStr = string.Format("{0:000}", absLon);
double fraction = (absLon - (int)absLon) * 60;
string fracStr = string.Format("{0:00.00}", fraction);
string ew = lon < 0 ? "W" : "E";
string result = $"{degStr}{fracStr}{ew}";
return result;
}
/// <summary>
/// Fixed 8 character field.
/// In degrees and decimal minutes to two DP, followed by N or S.
/// </summary>
/// <param name="lat"></param>
/// <returns></returns>
static string BuildLatString(double lat)
{
double absLat = Math.Abs(lat);
string degStr = string.Format("{0:00}", absLat);
double fraction = (absLat - (int)absLat) * 60;
string fracStr = string.Format("{0:00.00}", fraction);
string ew = lat > 0 ? "N" : "S";
string result = $"{degStr}{fracStr}{ew}";
return result;
}
static byte[] AssemblePacket(string srcCall, string destCall = "CQ", string[] digiCalls = null, string msg = null)
{
if (digiCalls == null)
{
digiCalls = new string[0];
}
var buffer = new List<byte>();
buffer.AddRange(BuildAddressFieldBytes(srcCall, destCall, digiCalls));
buffer.Add(0x03);
buffer.Add(0xf0);
buffer.AddRange(Escape(Encoding.ASCII.GetBytes(msg)));
// appears not to be necessary...
//byte[] fcs = CalculateFcs(buffer.ToArray());
//buffer.AddRange(fcs);
return buffer.ToArray();
}
static byte[] BuildAddressFieldBytes(string srcCall, string destCall, string[] digiCalls)
{
var arr = new List<byte>();
byte[] destCallBytes = GetCallsignBytes(destCall, false);
arr.AddRange(destCallBytes);
byte[] srcCallBytes = GetCallsignBytes(srcCall, digiCalls.Length == 0);
arr.AddRange(srcCallBytes);
for (int i = 0; i < digiCalls.Length; i++)
{
string digiCall = digiCalls[i];
byte[] digiCallBytes = GetCallsignBytes(digiCall, i == digiCalls.Length - 1);
arr.AddRange(digiCallBytes);
}
return arr.ToArray();
}
static byte[] GetCallsignBytes(string callAndSsid, bool isLastCallsign)
{
string call;
int ssid = 0;
if (callAndSsid.Contains("-"))
{
string[] comps = callAndSsid.Split('-');
call = comps[0];
ssid = int.Parse(comps[1]);
}
else
{
call = callAndSsid;
}
while (call.Length < 6)
{
call += " ";
}
var result = new List<byte>();
foreach (byte letter in call)
{
result.Add((byte)(letter << 1));
}
BitArray ssidBits = new BitArray(new[] { (byte)ssid });
BitArray ssidByte = new BitArray(8);
ssidByte[0] = false;
ssidByte[1] = true;
ssidByte[2] = true;
ssidByte[3] = ssidBits[3];
ssidByte[4] = ssidBits[2];
ssidByte[5] = ssidBits[1];
ssidByte[6] = ssidBits[0];
ssidByte[7] = isLastCallsign;
byte[] methodResult = new byte[7];
result.CopyTo(methodResult);
Reverse(ssidByte);
methodResult[6] = getIntFromBitArray(ssidByte);
return methodResult;
}
static void Reverse(BitArray array)
{
int length = array.Length;
int mid = (length / 2);
for (int i = 0; i < mid; i++)
{
bool bit = array[i];
array[i] = array[length - i - 1];
array[length - i - 1] = bit;
}
}
static byte getIntFromBitArray(BitArray bitArray)
{
if (bitArray.Length > 8)
throw new ArgumentException("Argument length shall be at most 8 bits.");
byte[] array = new byte[1];
bitArray.CopyTo(array, 0);
return array[0];
}
static byte[] Escape(byte[] v)
{
#warning This needs to implement the escaping described at https://en.wikipedia.org/wiki/KISS_(TNC)#Description - or does it? Needs testing...
return v;
}
static readonly Dictionary<Symbol, SymbolCode> SymbolTable = new Dictionary<Symbol, SymbolCode> {
{ Symbol.House, new SymbolCode('/', '-') },
{ Symbol.Digipeater, new SymbolCode('/', '#') },
{ Symbol.Phone, new SymbolCode('/', '$') },
{ Symbol.BoyScouts, new SymbolCode('/', ',') },
{ Symbol.RedCross, new SymbolCode('/', '+') },
{ Symbol.Motorcycle, new SymbolCode('/', '<') },
{ Symbol.Car, new SymbolCode('/', '>') },
{ Symbol.Canoe, new SymbolCode('/', 'C') },
{ Symbol.Hotel, new SymbolCode('/', 'H') },
{ Symbol.School, new SymbolCode('/', 'K') },
{ Symbol.Bus, new SymbolCode('/', 'U') },
{ Symbol.Yacht, new SymbolCode('/', 'Y') },
{ Symbol.Jogger, new SymbolCode('/', '[') },
{ Symbol.Ambulance, new SymbolCode('/', 'a') },
{ Symbol.Bicycle, new SymbolCode('/', 'b') },
{ Symbol.Jeep, new SymbolCode('/', 'j') },
{ Symbol.Truck, new SymbolCode('/', 'k') },
{ Symbol.EmergencyOpsCentre, new SymbolCode('/', 'o') },
{ Symbol.Antenna, new SymbolCode('/', 'r') },
{ Symbol.PowerBoat, new SymbolCode('/', 's') },
{ Symbol.Truck18Wheeler, new SymbolCode('/', 'u') },
{ Symbol.Van, new SymbolCode('/', 'v') },
{ Symbol.Water, new SymbolCode('/', 'w') },
{ Symbol.Restrooms, new SymbolCode('\\', 'r') },
};
}
internal class SymbolCode
{
public SymbolCode(char table, char code)
{
Table = table;
Code = code;
}
public char Table { get; set; }
public char Code { get; set; }
}
enum Symbol
{
House,
Digipeater,
Phone,
BoyScouts,
RedCross,
Motorcycle,
Car,
Canoe,
Hotel,
School,
Bus,
Yacht,
Jogger,
Ambulance,
Bicycle,
Jeep,
Truck,
EmergencyOpsCentre,
Antenna,
PowerBoat,
Truck18Wheeler,
Van,
Water,
Restrooms,
}
static class Extensions
{
public static void Write(this SerialPort sp, byte b)
{
Write(sp, new byte[] { b });
}
public static void Write(this SerialPort sp, byte[] b)
{
sp.Write(b, 0, b.Length);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment