///|
pub(all) struct FrameLimits {
max_sentence_bytes : Int
max_fields : Int
max_field_bytes : Int
} derive(Eq, Debug)
///|
pub fn FrameLimits::standard() -> FrameLimits {
{ max_sentence_bytes: 1024, max_fields: 64, max_field_bytes: 256 }
}
///|
pub(all) struct NmeaFrame {
prefix_value : UInt16
talker_value : String
formatter_value : String
fields_value : Array[String]
checksum_status_value : ChecksumStatus
checksum_value : Int?
} derive(Eq, Debug)
///|
pub fn NmeaFrame::new(
prefix : UInt16,
talker : String,
formatter : String,
fields : Array[String],
) -> NmeaFrame {
{
prefix_value: prefix,
talker_value: talker,
formatter_value: formatter,
fields_value: fields.copy(),
checksum_status_value: NotChecked,
checksum_value: None,
}
}
///|
pub fn NmeaFrame::prefix(self : NmeaFrame) -> UInt16 {
self.prefix_value
}
///|
pub fn NmeaFrame::talker(self : NmeaFrame) -> String {
self.talker_value
}
///|
pub fn NmeaFrame::formatter(self : NmeaFrame) -> String {
self.formatter_value
}
///|
pub fn NmeaFrame::fields(self : NmeaFrame) -> Array[String] {
self.fields_value.copy()
}
///|
pub fn NmeaFrame::checksum_status(self : NmeaFrame) -> ChecksumStatus {
self.checksum_status_value
}
///|
pub fn NmeaFrame::encode(self : NmeaFrame) -> String {
let mut payload = self.talker_value + self.formatter_value
for field in self.fields_value {
payload = payload + "," + field
}
(if self.prefix_value == 36 { "$" } else { "!" }) +
payload +
"*" +
NmeaChecksum::hex(NmeaChecksum::calculate(payload)) +
"\r\n"
}
///|
fn frame_error(code : NmeaErrorCode, message : String) -> NmeaError {
NmeaError::from_diagnostic(
Diagnostic::new(code, Error, message, SourceRef::sentence(0)),
)
}
///|
fn trim_line_end(text : String) -> String {
let mut end = text.length()
while end > 0 &&
(text[end - 1].to_int() == 10 || text[end - 1].to_int() == 13) {
end = end - 1
}
text[:end].to_owned()
}
///|
/// Parses one complete caller-provided NMEA frame.
pub fn parse_frame(
input : String,
policy? : ParsePolicy = Strict,
limits? : FrameLimits = FrameLimits::standard(),
) -> Result[NmeaFrame, NmeaError] {
let text = trim_line_end(input)
if text.length() < 2 {
return Err(frame_error(FramingMissingStart, "sentence is too short"))
}
if text.length() > limits.max_sentence_bytes {
return Err(
frame_error(FramingTooLong, "sentence exceeds configured length"),
)
}
let prefix = text[0]
if prefix != 36 && prefix != 33 {
return Err(
frame_error(FramingInvalidPrefix, "sentence must start with $ or !"),
)
}
let body = text[1:]
let mut star = -1
for index = 0; index < body.length(); index = index + 1 {
if body[index].to_int() == 42 {
star = index
break
}
}
let (payload, checksum_status, checksum) = if star < 0 {
if policy is Strict {
return Err(
frame_error(ChecksumMissing, "strict policy requires a checksum"),
)
}
(body.to_owned(), MissingChecksum, None)
} else {
let payload = body[:star].to_owned()
let expected_text = body[star + 1:].to_owned()
let expected = match checksum_value(expected_text) {
Ok(value) => value
Err(error) => return Err(error)
}
let actual = NmeaChecksum::calculate(payload)
if actual != expected {
return Err(
frame_error(ChecksumMismatch, "checksum does not match payload"),
)
}
(payload, PresentValid, Some(expected))
}
let parts : Array[String] = []
for part in payload.split(",") {
parts.push(part.to_owned())
}
if parts.length() == 0 || parts[0].length() < 5 {
return Err(
frame_error(
SentenceInvalid,
"sentence formatter must contain talker and formatter",
),
)
}
let identifier = parts[0]
let talker = identifier[:2].to_owned()
let formatter = identifier[2:].to_owned()
if formatter.length() == 0 {
return Err(frame_error(SentenceInvalid, "sentence formatter is empty"))
}
let fields : Array[String] = []
for index = 1; index < parts.length(); index = index + 1 {
let field = parts[index]
if field.length() > limits.max_field_bytes {
return Err(frame_error(FramingTooLong, "field exceeds configured length"))
}
fields.push(field)
}
if fields.length() > limits.max_fields {
return Err(frame_error(FramingTooLong, "sentence contains too many fields"))
}
Ok({
prefix_value: prefix,
talker_value: talker,
formatter_value: formatter,
fields_value: fields,
checksum_status_value: checksum_status,
checksum_value: checksum,
})
}