Last active
April 14, 2017 21:59
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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