SerialLoopback
the physical-computing loop with no board: a fake device on one side of a pty pair prints a sensor number thirty times a second, and a real SerialPort reads the other side, so the trace shows what arrived over the wire. A click writes a line back and the wave flips (SerialPort, latestLine)
- Run
swift run --package-path Examples Example-Integration-SerialLoopback- Imports
OllinSerial- Source
- Examples/Integration/SerialLoopback/Sketch.swift
import Foundation
import Ollin
import OllinSerial
/// The classic physical-computing loop, self-contained. The sketch runs *both*
/// ends of the wire: a tiny fake device (the manager side of a pty pair)
/// prints a sensor-ish number thirty times a second, and a `SerialPort` opens
/// the other side exactly the way it would open a USB microcontroller. The
/// trace is drawn from what arrives over the port, so what you see is the
/// serial round trip, not a local variable. Click to send a line back: the
/// fake device flips its wave upside down, which is the write path working.
///
/// To read a real board instead, see the `SerialMonitor` example. This
/// sketch's serial-facing code is the same either way; only the device is
/// fake.
@main
final class SerialLoopback: Sketch {
var device: FakeDevice!
var port: SerialPort!
var trace: [Double] = []
var received = 0
override func setup() {
device = FakeDevice()
port = SerialPort(path: device.path, baudRate: 9600)
port.open()
}
override func draw() {
background(Color(white: 0.06))
// The drain: every line since the last frame, one trace point each,
// so the wave is exactly the stream no matter the frame rate.
for line in port.lines() {
received += 1
if let sample = Double(line) { trace.append(sample) }
}
if trace.count > 240 { trace.removeFirst(trace.count - 240) }
// The polling cache: the latest value, read whenever this frame lands.
let value = port.number(default: 512)
// The trace, pinned to the right edge and growing in leftward.
let points = trace.enumerated().map { index, sample in
let slot = 239 - (trace.count - 1 - index)
return Vector2(
width * Double(slot) / 239,
height * 0.62 - (sample / 1023 - 0.5) * height * 0.5
)
}
noFill()
stroke(CosinePalette.neon.color(at: 0.3 + value / 1023 * 0.4))
strokeWeight(4 * scale)
drawPolyline(points)
if let tip = points.last {
noStroke()
fill(.white)
drawCircle(center: tip, radius: 10 * scale)
}
noStroke()
fill(Color(white: 0.6))
textSize(22 * scale)
drawText("serial ↺ \(port.isOpen ? device.path : "opening…")", 30 * scale, 50 * scale)
drawText("latest \(Int(value)) lines \(received) click to flip the wave", 30 * scale, 84 * scale)
}
override func mousePressed() {
port.writeLine("flip")
}
}
/// The device end of the wire: the manager side of a pty pair, printing a
/// wobbly value on a timer and flipping the wave when any line comes back.
final class FakeDevice: @unchecked Sendable {
let path: String
private let manager: Int32
private let queue = DispatchQueue(label: "serial-loopback.device")
private let timer: DispatchSourceTimer
private var clock = 0.0
private var flipped = false
init() {
let descriptor = posix_openpt(O_RDWR | O_NOCTTY)
precondition(
descriptor >= 0 && grantpt(descriptor) == 0 && unlockpt(descriptor) == 0,
"could not create the pty pair"
)
var settings = termios()
if tcgetattr(descriptor, &settings) == 0 {
cfmakeraw(&settings)
_ = tcsetattr(descriptor, TCSANOW, &settings)
}
_ = fcntl(descriptor, F_SETFL, fcntl(descriptor, F_GETFL) | O_NONBLOCK)
manager = descriptor
path = String(cString: ptsname(descriptor))
timer = DispatchSource.makeTimerSource(queue: queue)
timer.schedule(deadline: .now(), repeating: 1.0 / 30.0)
timer.setEventHandler { [weak self] in self?.tick() }
timer.activate()
}
private func tick() {
// Anything sent back flips the wave.
var buffer = [UInt8](repeating: 0, count: 256)
if read(manager, &buffer, buffer.count) > 0 { flipped.toggle() }
clock += 1.0 / 30.0
var wave = sin(clock * 1.4) * 0.8 + sin(clock * 4.3) * 0.2
if flipped { wave = -wave }
let value = Int((wave * 0.5 + 0.5) * 1023)
let line = "\(value)\n"
_ = line.withCString { write(manager, $0, strlen($0)) }
}
}