364 lines
11 KiB
Swift
364 lines
11 KiB
Swift
import Foundation
|
|
|
|
struct Constants{
|
|
static let SYNC: UInt8 = 0xA5
|
|
static let SYNC2: UInt8 = 0x5A
|
|
|
|
static let GET_INFO: UInt8 = 0x50
|
|
static let GET_HEALTH: UInt8 = 0x52
|
|
|
|
static let STOP: UInt8 = 0x25
|
|
static let RESET: UInt8 = 0x40
|
|
|
|
static let SCAN: UInt8 = 0x20
|
|
static let FORCE_SCAN: UInt8 = 0x21
|
|
|
|
static let DESCRIPTOR_LEN = 7
|
|
static let INFO_LEN = 20
|
|
static let HEALTH_LEN = 3
|
|
|
|
static let INFO_TYPE: UInt8 = 4
|
|
static let HEALTH_TYPE: UInt8 = 6
|
|
static let SCAN_TYPE: UInt8 = 129
|
|
|
|
static let SET_PWM_BYTE: UInt8 = 0xF0
|
|
static let MAX_MOTOR_PWM = 1023
|
|
static let DEFAULT_MOTOR_PWM = 660
|
|
}
|
|
|
|
public enum HEALTH_STATUSES: UInt8 {
|
|
case GOOD = 0, WARNING, ERROR
|
|
}
|
|
|
|
public enum RPLidarError: Error {
|
|
case FAILED_TO_START,
|
|
INCORRECT_INFO_FORMAT,
|
|
INCORRECT_HEALTH_FORMAT,
|
|
SCAN_ERROR(message: String),
|
|
INCORRECT_DESCRIPTOR_FORMAT
|
|
}
|
|
|
|
/**
|
|
Calculates the quality, angle and distance of a scan from the raw Data.
|
|
|
|
- returns:
|
|
LidarScan containing the quality, angle in degrees, and distance in mm.
|
|
|
|
- throws:
|
|
RPLidarError.SCAN_ERROR if the raw input is malformed or empty.
|
|
|
|
- parameters:
|
|
- raw: The bytes containing the scan data.
|
|
|
|
*/
|
|
func processScan(raw: Data) throws -> LidarScan {
|
|
let newScan = raw[0] & 0b1
|
|
let inversedNewScan = (raw[0] >> 1) & 0b1
|
|
let quality = raw[0] >> 2
|
|
if newScan == inversedNewScan {
|
|
throw RPLidarError.SCAN_ERROR(message: "New scan flags mismatch")
|
|
}
|
|
if (raw[1] & 0b1) != 1 {
|
|
throw RPLidarError.SCAN_ERROR(message: "Check bit not equal to 1")
|
|
}
|
|
let angle = Float((Int(raw[1]) >> 1) + (Int(raw[2]) << 7)) / 64
|
|
let distance = Float(Int(raw[3]) + (Int(raw[4]) << 8)) / 4
|
|
return LidarScan(newScan: newScan == 1, quality: quality, angle: angle, distance: distance)
|
|
}
|
|
|
|
public struct LidarScan{
|
|
public let newScan: Bool
|
|
public let quality: UInt8
|
|
public let angle: Float
|
|
public let distance: Float
|
|
}
|
|
|
|
|
|
/**
|
|
Function alias for measurement (single scan) closure. Use with iterMeasurements
|
|
|
|
- parameters:
|
|
- scan: Single lidar scan.
|
|
*/
|
|
public typealias MeasurementHandler = (_ scan: LidarScan) -> Bool
|
|
|
|
/**
|
|
Function alias for scan (multiple scans) closure. Use with iterScans
|
|
|
|
- parameters:
|
|
- scans: Array of lidar scans for this batch.
|
|
*/
|
|
public typealias ScanHandler = (_ scans: [LidarScan]) -> Bool
|
|
|
|
public class SwiftRPLidar {
|
|
private var motor: Bool = false
|
|
private var serialPort: LidarSerial
|
|
private var motorRunning = false
|
|
private let measurementDelayus: UInt32
|
|
|
|
/**
|
|
A class to interact with the RPLidar A1 and A2.
|
|
|
|
- parameters:
|
|
onPort: Serial Port the lidar is attached to
|
|
measurementDelayus: Delay that should be applied between each measurement. This is needed in case the serial port timeout is not working correctly.
|
|
|
|
- throws If the serial port cannot be connected, or the motor cannot be started.
|
|
*/
|
|
public init(onPort serialPort: LidarSerial, measurementDelayus: UInt32 = 500) throws {
|
|
self.serialPort = serialPort
|
|
self.measurementDelayus = measurementDelayus
|
|
try connect()
|
|
try startMotor()
|
|
}
|
|
|
|
deinit {
|
|
disconnect()
|
|
do {
|
|
try stop()
|
|
try stopMotor()
|
|
} catch {
|
|
print("Failed to stop lidar/motor.")
|
|
}
|
|
}
|
|
|
|
/**
|
|
Connect and configure the serial port.
|
|
*/
|
|
public func connect() throws {
|
|
disconnect()
|
|
try serialPort.openPort()
|
|
serialPort.setBaudrate(baudrate: 115200)
|
|
}
|
|
|
|
/**
|
|
Close the serial port.
|
|
*/
|
|
public func disconnect(){
|
|
// Need to close, SwiftSerial is blocking.
|
|
serialPort.closePort()
|
|
}
|
|
|
|
public func startMotor() throws{
|
|
// A1
|
|
serialPort.dtr = true
|
|
// A2
|
|
try self.setPwm(Constants.DEFAULT_MOTOR_PWM)
|
|
self.motorRunning = true
|
|
}
|
|
|
|
public func stopMotor() throws{
|
|
try setPwm(0)
|
|
serialPort.dtr = false
|
|
motorRunning = false
|
|
}
|
|
|
|
/**
|
|
Gets information about the connected lidar.
|
|
|
|
- returns (model, firmware, hardware, serialNumber)
|
|
*/
|
|
public func getInfo() throws -> (UInt8, (UInt8, UInt8), UInt8, String){
|
|
try sendCommand(Data([Constants.GET_HEALTH]))
|
|
let (dataSize, isSingle, dataType) = try readDescriptor()!
|
|
if dataSize != Constants.INFO_LEN || !isSingle || dataType != Constants.INFO_TYPE {
|
|
throw RPLidarError.INCORRECT_INFO_FORMAT
|
|
}
|
|
|
|
let raw = try readResponse(dataSize)
|
|
let serialNumber = String(bytes: raw[4...], encoding: .ascii)!
|
|
return (raw[0], (raw[2], raw[1]), raw[3], serialNumber)
|
|
}
|
|
|
|
/**
|
|
Get the current health status of the lidar.
|
|
|
|
- returns (Health status, errorCode
|
|
- throws If the received health status is in the incorrect format.
|
|
*/
|
|
public func getHealth() throws -> (HEALTH_STATUSES, UInt8){
|
|
try sendCommand(Constants.GET_HEALTH.asData())
|
|
guard let (dataSize, isSingle, dataType) = try readDescriptor() else {
|
|
throw RPLidarError.INCORRECT_HEALTH_FORMAT
|
|
}
|
|
if dataSize != Constants.HEALTH_LEN || !isSingle || dataType != Constants.HEALTH_TYPE{
|
|
throw RPLidarError.INCORRECT_HEALTH_FORMAT
|
|
}
|
|
let raw = try readResponse(dataSize)
|
|
let status = HEALTH_STATUSES(rawValue: raw[0])!
|
|
let errorCode = raw[1] << 8 + raw[2]
|
|
return (status, errorCode)
|
|
}
|
|
|
|
/**
|
|
Clear all data in the serial port buffer.
|
|
- throws If the buffer could not be emptied.
|
|
*/
|
|
public func clearInput() throws{
|
|
_ = try serialPort.readData(ofLength: serialPort.inWaiting)
|
|
}
|
|
|
|
/**
|
|
Send the stop command to the lidar.
|
|
- throws If the stop command cannot be sent.
|
|
*/
|
|
public func stop() throws{
|
|
try sendCommand(Constants.STOP.asData())
|
|
}
|
|
|
|
/**
|
|
Send the reset command to the lidar. This puts the lidar into a state as though it just connected and started scannning.
|
|
- throws If the reset command annot be reset.
|
|
*/
|
|
public func reset() throws{
|
|
try sendCommand(Constants.RESET.asData())
|
|
}
|
|
|
|
/**
|
|
Iterate over the lidar measurments, calling the given measurement handler each time scan is processed.
|
|
|
|
- parameters:
|
|
maxBufferMeasurements: Max number of measurements to include in the buffer before flushing it.
|
|
onMeasure: Measurement handler to execute on every read.
|
|
|
|
- throws If the input data is in the incorrect format, or the health retrieved errors.
|
|
*/
|
|
public func iterMeasurements(maxBufferMeasurements: Int = 500, _ onMeasure: MeasurementHandler) throws {
|
|
try startMotor()
|
|
let (status, _) = try getHealth()
|
|
if status == .ERROR{
|
|
throw RPLidarError.INCORRECT_HEALTH_FORMAT
|
|
}
|
|
|
|
else if status == .WARNING {
|
|
print("Warning given when checking health.")
|
|
}
|
|
|
|
try sendCommand(Constants.SCAN.asData())
|
|
let (dataSize, isSingle, dataType) = try readDescriptor()!
|
|
if dataSize != 5 || isSingle || dataType != Constants.SCAN_TYPE {
|
|
throw RPLidarError.INCORRECT_DESCRIPTOR_FORMAT
|
|
}
|
|
var read = true
|
|
while read {
|
|
do {
|
|
let raw = try readResponse(Int(dataSize))
|
|
if maxBufferMeasurements > 0 {
|
|
let dataInWaiting = serialPort.inWaiting
|
|
if dataInWaiting > maxBufferMeasurements {
|
|
print("Too many measurements in the input buffer. Clearing Buffer")
|
|
_ = try serialPort.readData(ofLength: dataInWaiting / Int(dataSize) * Int(dataSize))
|
|
}
|
|
}
|
|
if raw.count > 0 {
|
|
read = try onMeasure(processScan(raw: raw))
|
|
}
|
|
if measurementDelayus > 0 {
|
|
usleep(measurementDelayus)
|
|
}
|
|
} catch RPLidarError.INCORRECT_DESCRIPTOR_FORMAT {
|
|
print("Incorrect Descriptor, skipping scan")
|
|
} catch RPLidarError.SCAN_ERROR(let message) {
|
|
print("Scan processing failed, skipping scan: \(message)")
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Iterate over batches of measurements.
|
|
- parameters:
|
|
maxBufferMeasurements: Max number of measurements to include in the buffer before flushing it.
|
|
minLength: Minimum number of measurements to include in a scan.
|
|
onScan: Handler function to execute on each scan.
|
|
|
|
- throws If the input data is in the incorrect format, or the health retrieved errors.
|
|
*/
|
|
public func iterScans(maxBufferMeasurements: Int = 500, minLength: Int = 5, _ onScan: ScanHandler) throws {
|
|
var scan: [LidarScan] = []
|
|
var read = true
|
|
try iterMeasurements(maxBufferMeasurements: maxBufferMeasurements){ measurement in
|
|
if measurement.newScan {
|
|
if scan.count > minLength {
|
|
read = onScan(scan)
|
|
}
|
|
scan.removeAll()
|
|
}
|
|
if measurement.quality > 0 && measurement.distance > 0 {
|
|
scan.append(measurement)
|
|
}
|
|
return read
|
|
}
|
|
}
|
|
|
|
private func setPwm(_ pwm: Int) throws{
|
|
assert(0 <= pwm && pwm <= Constants.MAX_MOTOR_PWM)
|
|
try self.sendPayloadCommand(Constants.SET_PWM_BYTE, payload: pwm.asData())
|
|
}
|
|
|
|
private func sendPayloadCommand(_ cmd: Int, payload: Data) throws{
|
|
let size = UInt8(payload.count)
|
|
var req = Constants.SYNC.asData() + cmd.asData() + size.asData() + payload
|
|
let checksum = calcChecksum(req)
|
|
req += checksum.asData()
|
|
_ = try serialPort.writeData(req)
|
|
}
|
|
|
|
private func sendPayloadCommand(_ cmd: UInt8, payload: Data) throws {
|
|
return try sendPayloadCommand(Int(cmd), payload: payload)
|
|
}
|
|
|
|
private func calcChecksum(_ data: Data) -> Int{
|
|
var checksum = 0;
|
|
data.forEach { body in
|
|
checksum ^= Int(body)
|
|
}
|
|
return checksum
|
|
}
|
|
|
|
private func sendCommand(_ command: Data) throws{
|
|
var req = Constants.SYNC.asData()
|
|
req.append(command)
|
|
_ = try serialPort.writeData(req)
|
|
}
|
|
|
|
private func readDescriptor() throws -> (UInt8, Bool, UInt8)?{
|
|
let descriptor = try serialPort.readData(ofLength: Constants.DESCRIPTOR_LEN)
|
|
if (descriptor.count != Constants.DESCRIPTOR_LEN){
|
|
return nil
|
|
}
|
|
else if (descriptor[0...1] != Data([Constants.SYNC, Constants.SYNC2])){
|
|
return nil
|
|
}
|
|
let isSingle = descriptor[descriptor.count - 2] == 0
|
|
return (descriptor[2], isSingle, descriptor[descriptor.count - 1])
|
|
}
|
|
|
|
private func readResponse(_ dataSize: Int) throws -> Data {
|
|
let data = try serialPort.readData(ofLength: dataSize)
|
|
if(data.count != dataSize){
|
|
throw RPLidarError.INCORRECT_DESCRIPTOR_FORMAT
|
|
}
|
|
return data
|
|
}
|
|
|
|
private func readResponse(_ dataSize: UInt8) throws -> Data {
|
|
return try readResponse(Int(dataSize))
|
|
}
|
|
}
|
|
|
|
// MARK: Convenience packing extensions.
|
|
|
|
extension Int{
|
|
func asData() -> Data {
|
|
var int = self
|
|
return Data(bytes: &int, count: int / Int(UINT8_MAX))
|
|
}
|
|
}
|
|
|
|
extension UInt8 {
|
|
func asData() -> Data {
|
|
return Data([self])
|
|
}
|
|
}
|
|
|