<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
	<channel>
		<title>Tristan Penman&apos;s Blog</title>
		<description>A blog about the fun parts of programming. I write about C++ and Ruby, retro game development, and occassionaly dabble in algorithms and data structures.</description>
		<link>https://tristanpenman.com</link>
		<atom:link href="https://tristanpenman.com/feed.xml" rel="self" type="application/rss+xml" />
		
			
				<item>
					<title>Robotics in Go: Building a Raspberry Pi Rover </title>
					
					<description>&lt;p&gt;When we consider programming languages for robotics, those that come to mind often include established players such as C and C++, Python, or even those specifically designed for scientific and numerical computing, such as MATLAB. In this post, we look at Go as a viable programming language for robotics.&lt;/p&gt;

&lt;p&gt;This post builds upon content I presented at the &lt;a href=&quot;https://www.meetup.com/melbourne-go-nuts/&quot;&gt;Melbourne Go Meetup&lt;/a&gt; on May 14, 2026. My talk centred around a project called &lt;em&gt;Rover Kit&lt;/em&gt;, a small rover that uses Go for motor control and sonar readings. The Rover Kit source code has been published &lt;a href=&quot;https://github.com/tristanpenman/rover-kit&quot;&gt;on GitHub&lt;/a&gt;. The &lt;a href=&quot;https://github.com/tristanpenman/rover-kit/releases/tag/v0.1.0&quot;&gt;Rover Kit v0.1.0&lt;/a&gt; release contains the version of the code discussed below.&lt;/p&gt;

&lt;h3 id=&quot;contents&quot;&gt;Contents&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#introduction&quot;&gt;Introduction&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#the-hardware&quot;&gt;The Hardware&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#robotics-fundamentals&quot;&gt;Robotics Fundamentals&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#robot-operating-system&quot;&gt;Robot Operating System&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#message-passing&quot;&gt;Message Passing&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#bare-metal-robotics&quot;&gt;Bare Metal Robotics&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#go-for-robotics&quot;&gt;Go for Robotics&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#concurrency-model&quot;&gt;Concurrency Model&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#cross-compilation&quot;&gt;Cross-Compilation&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#go-drawbacks&quot;&gt;Go Drawbacks&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#hardware-interfaces&quot;&gt;Hardware Interfaces&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#gpio&quot;&gt;GPIO&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#uart&quot;&gt;UART&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#spi&quot;&gt;SPI&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#ic&quot;&gt;I²C&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#go-libraries&quot;&gt;Go Libraries&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#gobot&quot;&gt;Gobot&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#periph&quot;&gt;Periph&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#serial&quot;&gt;Serial&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#rover-kit&quot;&gt;Rover Kit&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#mqtt-client&quot;&gt;MQTT Client&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#peripherals&quot;&gt;Peripherals&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#project-structure&quot;&gt;Project Structure&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#motor-control&quot;&gt;Motor Control&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#pulse-width-modulation&quot;&gt;Pulse Width Modulation&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#gobot-driver&quot;&gt;Gobot Driver&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#periph-driver&quot;&gt;Periph Driver&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#sonar-reader&quot;&gt;Sonar Reader&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#sonar-sensors&quot;&gt;Sonar Sensors&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#voltage-divider&quot;&gt;Voltage Divider&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#periph-provider&quot;&gt;Periph Provider&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#uart-provider&quot;&gt;UART Provider&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#web-bridge&quot;&gt;Web Bridge&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#websocket-server&quot;&gt;WebSocket Server&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#gamepad-support&quot;&gt;Gamepad Support&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#next-steps&quot;&gt;Next Steps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#references&quot;&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;So much of the appeal of robotics is its breadth as a subject. It touches upon mathematics, mechanical and electrical engineering, and (of course) software. Just as broad are the many different kinds of robots we can build. The most ambitious include bipedal humanoid robots, actuated robots that operate in factories, or even surgical devices.&lt;/p&gt;

&lt;h3 id=&quot;the-hardware&quot;&gt;The Hardware&lt;/h3&gt;

&lt;p&gt;This post focuses on a small rover that includes four DC motors (driven by an Adafruit Motor HAT) and several HC-SR04 ultrasonic distance sensors. The chassis and wheels are from a &lt;a href=&quot;https://www.servocity.com/whippersnapper-runt-rover/&quot;&gt;Whippersnapper Runt Rover&lt;/a&gt; kit. With everything connected, the rover looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/rover-kit-hardware.jpg&quot; alt=&quot;rover kit hardware&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Hidden within the chassis is a 12V battery pack to power the DC motors.&lt;/p&gt;

&lt;p&gt;It also includes a 5V USB power supply that powers a &lt;em&gt;Raspberry Pi Zero W&lt;/em&gt; - this is the brains of the operation:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/raspberry-pi-zero-w.png&quot; alt=&quot;raspberry pi zero w&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Although fairly modest as far as robotics is concerned, this kit allows us to explore the software stack while steering clear of advanced topics (e.g. inverse kinematics and frames of reference).&lt;/p&gt;

&lt;h3 id=&quot;robotics-fundamentals&quot;&gt;Robotics Fundamentals&lt;/h3&gt;

&lt;p&gt;A typical robot incorporates actuation (wheels, motors, or other moving parts), sensors to perceive the environment, and some degree of planning and control. Tying this all together are software systems that ensure the robot achieves the task it was designed to do.&lt;/p&gt;

&lt;p&gt;The demands placed on robotics software are non-trivial. They include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Real-time control - operating within precise timing constraints&lt;/li&gt;
  &lt;li&gt;Embedded environments - low power and harsh operating conditions (heat, dust, moisture)&lt;/li&gt;
  &lt;li&gt;Probabilistic planning - working with sensor data that might be noisy or incomplete&lt;/li&gt;
  &lt;li&gt;Safety systems and critical infrastructure - ensuring safe and correct operation, under a wide range of conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To address these concerns, large-scale hardware projects will often turn to a mature robotics framework that offers component isolation, message passing, device drivers, diagnostics, and deployment tooling.&lt;/p&gt;

&lt;p&gt;A popular choice today is ROS, otherwise known as the &lt;em&gt;Robot Operating System&lt;/em&gt;.&lt;/p&gt;

&lt;h3 id=&quot;robot-operating-system&quot;&gt;Robot Operating System&lt;/h3&gt;

&lt;p&gt;ROS provides middleware, standard message types, navigation and perception packages, and tools for visualisation and debugging. It also offers broad device support, which makes it appealing for research and prototyping.&lt;/p&gt;

&lt;p&gt;ROS has historically had two major release lines. ROS 1 was built around a central &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;roscore&lt;/code&gt; process, but reached end-of-life in 2025. ROS 2 moved away from that central control node architecture and added features aimed at commercial and industrial robotics, including QoS, device discovery, and improved security.&lt;/p&gt;

&lt;h3 id=&quot;message-passing&quot;&gt;Message Passing&lt;/h3&gt;

&lt;p&gt;A common theme, though, is message passing. ROS 1 and ROS 2 are both designed around message passing as a core concept. Individual components can be responsible for just one aspect of a robot’s behaviour, with components coordinating towards larger goals by publishing and subscribing to different topics on a message bus:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/message-bus.png&quot; alt=&quot;message bus&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Although the message passing concept has proven scalable and robust, ROS itself has a steep learning curve. It also comes with resource demands that may strain small devices. This complexity may distract from the actual goal of a hobbyist: having fun with a project that combines hardware and software. For that we want something more akin to &lt;em&gt;“bare metal robotics”&lt;/em&gt;.&lt;/p&gt;

&lt;h2 id=&quot;bare-metal-robotics&quot;&gt;Bare Metal Robotics&lt;/h2&gt;

&lt;p&gt;Bare metal robotics captures the notion of doing robotics with as little overhead as possible.&lt;/p&gt;

&lt;p&gt;Python is a common choice for hobbyist robotics, thanks in part to its simplicity. Python features a wide range of libraries for GPIO communication and other hardware interfaces. However, a major drawback is performance, as it suffers from being an interpreted language. Deployment can also be challenging, due to the number of dependencies that must be installed on the target device.&lt;/p&gt;

&lt;h3 id=&quot;go-for-robotics&quot;&gt;Go for Robotics&lt;/h3&gt;

&lt;p&gt;It turns out that Go is also a great option for bare metal robotics.&lt;/p&gt;

&lt;p&gt;While it may not be perfect, Go has several properties that make it an appealing choice:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Concurrency model - goroutines and channels offer a clean mapping to asynchronous sensor/actuator loops&lt;/li&gt;
  &lt;li&gt;Memory safety - no pointer arithmetic and a memory-safe runtime make it safer than C++ (for many classes of bugs… in theory)&lt;/li&gt;
  &lt;li&gt;Simplicity &amp;amp; readability - Go is often easier to read than C++ or Python&lt;/li&gt;
  &lt;li&gt;Cross-compilation - often produces self-contained binaries with minimal runtime dependencies&lt;/li&gt;
  &lt;li&gt;Iteration - fast builds, simple dependency management, good testing support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The concurrency model and support for cross-compilation are two of the most interesting properties (in my opinion!).&lt;/p&gt;

&lt;h3 id=&quot;concurrency-model&quot;&gt;Concurrency Model&lt;/h3&gt;

&lt;p&gt;Robots combine multiple activities that are conceptually independent, but must coordinate with one another to achieve various tasks. A simple robot may combine sensors, actuators (e.g. motor controllers) and basic navigation. Each of these components (or processes) needs to run concurrently.&lt;/p&gt;

&lt;p&gt;Go’s concurrency model maps nicely to that problem. A goroutine can own a long-running activity, such as sampling a sonar sensor once per second, while a channel becomes the handoff point for the values it produces. In turn, Go’s treatment of channels as first-class language features leads to several nice concurrency patterns.&lt;/p&gt;

&lt;p&gt;Go’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;context&lt;/code&gt; package also provides clean shutdown semantics. Commands can create a context tied to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SIGINT&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SIGTERM&lt;/code&gt;, pass it down into individual goroutines, and block until cancellation.&lt;/p&gt;

&lt;h3 id=&quot;cross-compilation&quot;&gt;Cross-Compilation&lt;/h3&gt;

&lt;p&gt;Cross-compilation is another major benefit! The Go compiler makes it extremely easy to cross-compile binaries that run natively on the Raspberry Pi Zero W.&lt;/p&gt;

&lt;p&gt;The compiler provides built-in support for ARMv6, which is the instruction set used by the Pi Zero. Cross-compilation is as simple as running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go build&lt;/code&gt; on your host machine with the appropriate command line arguments and environment variables:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;env &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;GOOS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux &lt;span class=&quot;nv&quot;&gt;GOARCH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;arm &lt;span class=&quot;nv&quot;&gt;GOARM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;6 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  go build &lt;span class=&quot;nt&quot;&gt;-ldflags&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;-w&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; bin/motor-control cmd/motor-control/main.go
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;go-drawbacks&quot;&gt;Go Drawbacks&lt;/h3&gt;

&lt;p&gt;The primary drawback of Go is the garbage collector (GC), which can lead to unpredictable latency spikes. Timing discrepancies can impact perception and coordination. For certain motor configurations, this could lead to overshooting or undershooting actuation targets.&lt;/p&gt;

&lt;p&gt;While it’s possible to configure garbage collection, or even switch it off entirely, these are beyond the scope of this post.&lt;/p&gt;

&lt;h2 id=&quot;hardware-interfaces&quot;&gt;Hardware Interfaces&lt;/h2&gt;

&lt;p&gt;Embedded devices support a range of hardware I/O interfaces, each designed to accommodate different hardware design goals. A common characteristic of the interfaces described below is that they are all &lt;em&gt;serial&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Readers who are old enough may recall owning a computer that had both serial and parallel ports:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/serial-vs-parallel.jpg&quot; alt=&quot;serial vs parallel&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image credit: &lt;a href=&quot;https://www.geeksforgeeks.org/computer-organization-architecture/difference-between-serial-port-and-parallel-ports&quot;&gt;Difference between Serial Port and Parallel Port&lt;/a&gt; by GeeksForGeeks&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;An obvious difference between the two ports is the number of pins required. The parallel port has a wider bus, which means it can transmit more bits at the same time, while serial is designed to transmit one bit at a time.&lt;/p&gt;

&lt;p&gt;Serial may seem limiting, but it forms the foundation of various hardware interfaces. This includes the ubiquitous USB (Universal Serial Bus) and the GPIO pins commonly found on embedded devices.&lt;/p&gt;

&lt;h3 id=&quot;gpio&quot;&gt;GPIO&lt;/h3&gt;

&lt;p&gt;GPIO (or General-Purpose Input/Output) refers to a set of digital pins that can be controlled via software. In hobby electronics projects, it is common to wire up GPIO pins to LEDs, sensors, and other small components. Each SBC has its own GPIO pin configuration, such as this one for the Raspberry Pi:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/gpio-pinout.png&quot; alt=&quot;gpio pinout&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image credit: &lt;a href=&quot;https://www.seeedstudio.com/blog/2020/02/19/how-to-use-raspberry-pi-gpio-pins-python-tutorial&quot;&gt;How to Use Raspberry Pi GPIO Pins – Python Tutorial&lt;/a&gt; by Seeed Studio.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Groups of GPIO pins can be used to implement more sophisticated protocols or hardware interfaces. We’ll introduce some of those now.&lt;/p&gt;

&lt;h3 id=&quot;uart&quot;&gt;UART&lt;/h3&gt;

&lt;p&gt;One of the most common interfaces you’ll find on embedded devices is UART, short for Universal Asynchronous Receiver/Transmitter. UART is a protocol for serial communication between two devices, using separate transmit and receive signal wires plus a shared ground. Two UART devices are connected via cross-over wiring:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/uart-wiring.png&quot; alt=&quot;UART Wiring&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;For signalling purposes, each character is preceded by a start bit (always a zero) and ends with one or more stop bits (logical ones). The signal line is held in the ‘one’ state when nothing is being transmitted:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/uart-signal.png&quot; alt=&quot;UART Signal&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Standard UART data rates range from 9600 bits per second (bps) up to 115200 bps. High-speed UART implementations exist, but are less common. A more common alternative is USART, short for Universal Synchronous/Asynchronous Receiver-Transmitter, which includes an additional clock wire for synchronisation, enabling faster transfer rates.&lt;/p&gt;

&lt;h3 id=&quot;spi&quot;&gt;SPI&lt;/h3&gt;

&lt;p&gt;SPI (Serial Peripheral Interface) is a fast synchronous serial bus commonly used to talk to chips on the same board. In a typical setup, there is one SPI &lt;em&gt;controller&lt;/em&gt;, which is responsible for communicating with one or more SPI &lt;em&gt;peripherals&lt;/em&gt;. Peripheral chips are each targeted using a dedicated &lt;em&gt;chip select&lt;/em&gt; pin, often labelled CS or SS.&lt;/p&gt;

&lt;p&gt;The following diagram shows three SPI peripherals wired up to a single SPI controller. Note the presence of three CS/SS pins:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/spi-bus.png&quot; alt=&quot;spi bus&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Examples of SPI peripherals include displays, sensors, and storage devices (think SD cards). When working with embedded devices you may encounter &lt;em&gt;SPI Flash&lt;/em&gt;, a kind of non-volatile flash memory that is capable of retaining data without power. SPI Flash is well suited to storing data and program code that needs to be loaded when a device is first powered up.&lt;/p&gt;

&lt;h3 id=&quot;ic&quot;&gt;I²C&lt;/h3&gt;

&lt;p&gt;I²C (&lt;em&gt;Inter-Integrated Circuit&lt;/em&gt;) is a two-wire synchronous serial bus designed for connecting multiple chips via the same circuit.&lt;/p&gt;

&lt;p&gt;Unlike SPI, I²C is an &lt;em&gt;addressed&lt;/em&gt; bus, which means that individual chips are targeted via an address encoded into the bitstream. Chips are connected to the bus via two wires: &lt;em&gt;serial data&lt;/em&gt; (SDA) and &lt;em&gt;serial clock&lt;/em&gt; (SCL). It is assumed that there is an &lt;em&gt;I²C controller&lt;/em&gt; which controls the bus and clocks data from target devices when reading.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/i2c-communication.png&quot; alt=&quot;i2c communication&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;A start of a frame consists of a 7-bit address, a read/write bit which indicates whether the controller is sending or receiving data, and an ACK bit. The control then sends data, or receives data, depending on the R/W bit.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The Adafruit Motor HAT in this project uses an I²C-based PWM driver chip. This means that, with just two signal wires connected to the Raspberry Pi, we can control four DC motors!&lt;/p&gt;

&lt;h2 id=&quot;go-libraries&quot;&gt;Go Libraries&lt;/h2&gt;

&lt;p&gt;We’ve seen above that Go can be used to target platforms such as the Raspberry Pi, BeagleBone or Jetson Nano, and we’ve introduced some of the hardware interfaces available on these devices. The next question is how we can make use of those hardware interfaces.&lt;/p&gt;

&lt;p&gt;This section will look at three in detail: &lt;em&gt;Gobot&lt;/em&gt;, &lt;em&gt;Periph&lt;/em&gt; and &lt;em&gt;Serial&lt;/em&gt;. These are the three libraries used in &lt;em&gt;Rover Kit&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;There are many others we could look at (such as &lt;a href=&quot;https://github.com/warthog618/go-gpiocdev&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpiocdev&lt;/code&gt;&lt;/a&gt; which gets an honourable mention), but unfortunately we don’t have time to cover them all.&lt;/p&gt;

&lt;h3 id=&quot;gobot&quot;&gt;Gobot&lt;/h3&gt;

&lt;p&gt;Gobot is a robotics framework for Go that provides APIs for devices, sensors, and actuators across a wide range of platforms. Using the provided drivers and adaptors, you can write higher-level behaviour once and retarget from one board or device family to another with minimal changes.&lt;/p&gt;

&lt;p&gt;The following example uses the &lt;a href=&quot;https://firmata.org/&quot;&gt;firmata&lt;/a&gt; adaptor to control an LED connected to an Arduino:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;package&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;main&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;firmataAdaptor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;firmata&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NewAdaptor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/dev/ttyACM0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;led&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gpio&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NewLedDriver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;firmataAdaptor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;13&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;work&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gobot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Every&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;led&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Toggle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;robot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gobot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NewRobot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;bot&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gobot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Connection&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;firmataAdaptor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gobot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Device&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;led&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;work&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;// Start the main event loop&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;robot&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Gobot provides adaptors for a wide range of SBC devices, including the Raspberry Pi, BeagleBone, and Tinker Board. It also supports drones and robotics kits.&lt;/p&gt;

&lt;h3 id=&quot;periph&quot;&gt;Periph&lt;/h3&gt;

&lt;p&gt;Periph is a low-level hardware I/O library for Go. It provides direct access to GPIO, I²C, SPI, PWM, and other interfaces commonly found on SBCs. It is especially useful when you want tight control of peripheral communication without needing a heavier robotics framework.&lt;/p&gt;

&lt;p&gt;We will see later that Periph can be used to drive motors via the Adafruit Motor HAT, and measure distance using the HC-SR04 ultrasonic sensors.&lt;/p&gt;

&lt;h3 id=&quot;serial&quot;&gt;Serial&lt;/h3&gt;

&lt;p&gt;The last library I want to mention is &lt;a href=&quot;https://github.com/bugst/go-serial&quot;&gt;go.bug.st/serial&lt;/a&gt;, a cross-platform serial port library for Go.&lt;/p&gt;

&lt;p&gt;As you can see, this library makes it easy to open and read from a serial device:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// configure&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;serial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;BaudRate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;115200&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;DataBits&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;StopBits&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;serial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;OneStopBit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;Parity&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;serial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NoParity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;// open port&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;serial&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/dev/ttyACM0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;// read bytes from UART connection&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;buff&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;make&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;128&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;bytesRead&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;buff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bytesRead&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This can be used to delegate reading from ultrasonic sensors to a separate microcontroller, which is something we’ll explore in a future post!&lt;/p&gt;

&lt;h2 id=&quot;rover-kit&quot;&gt;Rover Kit&lt;/h2&gt;

&lt;p&gt;In this section, we take a closer look at the hobby project that inspired this post: &lt;a href=&quot;https://github.com/tristanpenman/rover-kit.git&quot;&gt;Rover Kit&lt;/a&gt;. The project adopts a modular architecture, with separate Go executables to drive motors, read sonar sensors, and send commands via a web interface.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://mqtt.org/&quot;&gt;MQTT&lt;/a&gt; provides pub/sub messaging between components:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/rover-kit-architecture.png&quot; alt=&quot;rover kit architecture&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;MQTT is a standard protocol for communication between IoT devices. This is similar to the message bus we saw in ROS, and has the advantage of being highly portable. At the time of writing, there are &lt;a href=&quot;https://mqtt.org/software/&quot;&gt;over 20 server implementations&lt;/a&gt; and even more client libraries.&lt;/p&gt;

&lt;h3 id=&quot;mqtt-client&quot;&gt;MQTT Client&lt;/h3&gt;

&lt;p&gt;We make use of the &lt;a href=&quot;https://github.com/eclipse-paho/paho.mqtt.golang&quot;&gt;Eclipse Paho MQTT Go client&lt;/a&gt; to connect to an MQTT broker and pass messages between components.&lt;/p&gt;

&lt;p&gt;Basic connection:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mqtt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NewClient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;connectToken&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;connectToken&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Wait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;connectToken&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Fatalf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;failed to connect to broker=%s err=%v&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;brokerURL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Subscription example:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;opts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetOnConnectHandler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;client&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mqtt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Client&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;connected to broker=%s&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;brokerURL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;client&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Subscribe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;motorCmdTopic&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;subscriber&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Wait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;failed to subscribe topic=%s err=%v&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;motorCmdTopic&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;subscribed topic=%s&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;motorCmdTopic&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Publishing example:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mqttClient&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Publish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;defaultMotorCmdTopic&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;peripherals&quot;&gt;Peripherals&lt;/h3&gt;

&lt;p&gt;Recall that our rover hardware included:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Four DC motors&lt;/li&gt;
  &lt;li&gt;Four HC-SR04 ultrasonic distance sensors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The motor HAT is controlled over I²C through the Raspberry Pi’s GPIO header, while the ultrasonic sensors use ordinary GPIO pins. Rover Kit provides motor driver implementations for &lt;em&gt;Gobot&lt;/em&gt; and &lt;em&gt;Periph&lt;/em&gt;. Supporting both is not strictly necessary for such a simple project. However, it serves some pedagogical value by allowing us to compare the implementations side-by-side.&lt;/p&gt;

&lt;h3 id=&quot;project-structure&quot;&gt;Project Structure&lt;/h3&gt;

&lt;p&gt;Rover Kit follows a standard Go project structure. The three commands live in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd/&lt;/code&gt;, while supporting code lives in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pkg/&lt;/code&gt;. There is no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;internal/&lt;/code&gt; directory, as the project is intended to be completely open.&lt;/p&gt;

&lt;p&gt;You will also find configuration files for deployment on the Raspberry Pi:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deploy/&lt;/code&gt; - systemd configuration files&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mqtt/&lt;/code&gt; - mosquitto configuration files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Makefile&lt;/code&gt; has also been included, making it very easy to cross-compile for ARMv6.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;make
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, we’ll look at the three commands: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;motor-control&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sonar-reader&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;web-bridge&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;motor-control&quot;&gt;Motor Control&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;motor-control&lt;/code&gt; command knows how to interact with our DC motors, via the Adafruit Motor HAT. This is backed by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Driver&lt;/code&gt; interface, which declares the main behaviours that we need to support for our four DC motors:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Driver&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;Forwards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;Backwards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;SpinCW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;SpinCCW&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;Stop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;Close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;pulse-width-modulation&quot;&gt;Pulse Width Modulation&lt;/h3&gt;

&lt;p&gt;Pulse Width Modulation (or PWM) controls a DC motor by rapidly switching the motor’s power supply on and off instead of varying the voltage directly.&lt;/p&gt;

&lt;p&gt;You can picture this as a cycle. During the “on” part of each cycle, the motor receives 100% voltage. During the “off” part of the cycle, it receives no voltage. Because this happens so quickly, the motor behaves as if it is receiving an average voltage. The proportion of “on” vs “off” time is called the “duty cycle”. A 20% duty cycle means the motor is powered 20% of the time, and unpowered for the remaining 80%.&lt;/p&gt;

&lt;p&gt;The Adafruit Motor HAT handles this for us. However, we still need to tell it what the desired duty cycle is for each motor. This is communicated via I²C.&lt;/p&gt;

&lt;h3 id=&quot;gobot-driver&quot;&gt;Gobot Driver&lt;/h3&gt;

&lt;p&gt;Gobot provides an adaptor for the Raspberry Pi and an I²C driver for the Adafruit Motor HAT:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// Create an adaptor for the Raspberry Pi I2C interface&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;adaptor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;raspi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NewAdaptor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;adaptor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Connect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;connect raspi adaptor: %w&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;// Initialise the motor hat driver&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;hat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i2c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NewAdafruitMotorHatDriver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;adaptor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;i2c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WithBus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;i2c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WithAddress&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;defaultGobotMotorHatI2C&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;// Start the driver&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;adaptor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Finalize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;start Adafruit motor hat driver: %w&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Although Gobot also offers a high-level event loop, we won’t use it for such a simple project. We simply send commands to the motor HAT, using a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setMotor&lt;/code&gt; helper function:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GobotDriver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;setMotor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;throttle&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;math&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;math&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;throttle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;speed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;math&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;math&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Abs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;255&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;

	&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetDCMotorSpeed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;speed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
		&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RunDCMotor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i2c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AdafruitForward&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
		&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RunDCMotor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i2c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AdafruitBackward&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
		&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hat&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RunDCMotor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i2c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AdafruitRelease&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Using the helper simplifies the implementation of individual actions, as we can see in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Forwards&lt;/code&gt; below:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GobotDriver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Forwards&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Lock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;defer&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

	&lt;span class=&quot;n&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;float64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;range&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
		&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setMotor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
			&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;forwards motor %d: %w&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;periph-driver&quot;&gt;Periph Driver&lt;/h3&gt;

&lt;p&gt;Another option is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Periph&lt;/code&gt;, which also happens to support the &lt;a href=&quot;https://www.nxp.com/products/power-drivers/lighting-driver-and-controller-ics/led-drivers/16-channel-12-bit-pwm-fm-plus-ic-bus-led-driver:PCA9685&quot;&gt;PCA9685&lt;/a&gt; chip on the Adafruit Motor HAT.&lt;/p&gt;

&lt;p&gt;Unlike Gobot’s Motor HAT driver, we must manually specify the chip’s PWM motor configuration:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;periphMotorChannels&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;periphMotorChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pwm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pwm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;13&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;12&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pwm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pwm&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The equivalent &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setMotor&lt;/code&gt; helper is a little more involved. Some error handling has been omitted for brevity:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PeriphDriver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;setMotor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;throttle&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;periphMotorChannels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;unsupported motor index %d&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;periphMotorChannels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;motor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

	&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;math&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;math&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Min&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;throttle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;duty&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pwmDutyFromThrottle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

	&lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
		&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
		&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
		&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
		&lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;setChannel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duty&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pca&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetFullOff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pwm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pca&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SetPwm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pwm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;sonar-reader&quot;&gt;Sonar Reader&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sonar-reader&lt;/code&gt; command is responsible for taking readings from each of the HC-SR04 ultrasonic sensors. This is backed by the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Provider&lt;/code&gt; interface:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Reading&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;DistanceCM&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float64&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;DurationUS&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float64&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;Timestamp&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Time&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Provider&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;Open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;chan&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Reading&lt;/span&gt;
	&lt;span class=&quot;n&quot;&gt;Close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;sonar-sensors&quot;&gt;Sonar Sensors&lt;/h3&gt;

&lt;p&gt;Interacting with the HC-SR04 sensors is a little tricky, due to their timing sensitivity. Each sensor component has four pins, a transmitter (T / TX), and a receiver (R / RX):&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/hc-sr04-pinout.png&quot; alt=&quot;hc sr04 pinout&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;To measure the distance to the nearest obstacle, the transmitter first emits an ultrasonic pulse, then the receiver detects the echo of that pulse. The time between these two events is used to calculate the distance.&lt;/p&gt;

&lt;p&gt;We can tell the sensor to emit a pulse by holding the trigger pin high for a short period. The sensor then raises the echo pin for the duration of a reflected ultrasonic pulse. Measuring that echo duration gives the round-trip travel time. We multiply by the speed of sound to calculate the total distance, then divide by two since we only want the distance in one direction:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/hc-sr04-distance.png&quot; alt=&quot;hc sr04 distance&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;voltage-divider&quot;&gt;Voltage Divider&lt;/h3&gt;

&lt;p&gt;It’s also worth noting voltage requirements. The HC-SR04 accepts 3.3V or 5V on the trigger pin, but the echo pin outputs 5V. However, the GPIO pins on the Raspberry Pi are not 5V tolerant. This means we must use a voltage divider to drop the voltage on the echo pin to 3.3V.&lt;/p&gt;

&lt;p&gt;Voltage divider circuits are a dime-a-dozen online, and you’ll want to choose one that suits your particular hardware design. Here’s a photo from when I was halfway through soldering the small voltage divider board included in the rover:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/voltage-divider.jpg&quot; alt=&quot;voltage divider&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;This iteration of the board supported two HC-SR04 sensors.&lt;br /&gt;
The final board supports four HC-SR04 sensors.&lt;/small&gt;&lt;/p&gt;

&lt;h3 id=&quot;periph-provider&quot;&gt;Periph Provider&lt;/h3&gt;

&lt;p&gt;Using &lt;em&gt;Periph&lt;/em&gt;, we can interact with the HC-SR04 via GPIO pins on the Raspberry Pi:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// First we get references to appropriate GPIO pins:&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;trig&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gpioreg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ByName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;GPIO18&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gpioreg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ByName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;GPIO24&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;// Example: Set trigger pin to low&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;trig&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gpio&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Low&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;// Example: Read from the echo pin&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;echo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;In&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gpio&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Float&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gpio&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NoEdge&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To take sonar readings indefinitely, we use a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for&lt;/code&gt;-ever loop inside a goroutine:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// set trigger pin high&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trig&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gpio&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;High&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;// sleep for long enough that the sonar will register the signal&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Sleep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;triggerPulse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;// set trigger low&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trig&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Out&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gpio&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Low&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;// wait for echo high&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;echo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gpio&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;High&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Since&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;echoTimeout&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;timed out waiting for echo high&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;// wait for echo low&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;echo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gpio&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Low&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Since&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;echoTimeout&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;timed out waiting for echo low&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;c&quot;&gt;// calculate distance based on duration&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Sub&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;distance&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Seconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;soundSpeedCMPerS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Reading&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;DistanceCM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;distance&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;DurationUS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Microseconds&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()),&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;Timestamp&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Sleep&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sampleInterval&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;uart-provider&quot;&gt;UART Provider&lt;/h3&gt;

&lt;p&gt;Due to the timing-sensitive nature of ultrasonic readings, we also provide a UART provider. This can be used to gather readings from an external microcontroller (such as an STM32-based MCU).&lt;/p&gt;

&lt;p&gt;We will look at how this works in the next post.&lt;/p&gt;

&lt;h2 id=&quot;web-bridge&quot;&gt;Web Bridge&lt;/h2&gt;

&lt;p&gt;Finally, we have the Web Bridge, which allows us to control the rover from a web browser. The web interface is intentionally small. It consists of a single HTML page (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.html&lt;/code&gt;), which loads a stylesheet and vanilla JavaScript:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/web-bridge.png&quot; alt=&quot;Web Bridge&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;After loading, the web client opens a WebSocket connection back to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/ws&lt;/code&gt; on the same host. This connection sends rover commands to the server and displays messages received from the server, including distance measurements.&lt;/p&gt;

&lt;h3 id=&quot;websocket-server&quot;&gt;WebSocket Server&lt;/h3&gt;

&lt;p&gt;The WebSocket endpoint is where the web UI meets MQTT. The web bridge is responsible for validating messages and publishing them via MQTT.&lt;/p&gt;

&lt;p&gt;For example, when the browser sends the message &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;type&quot;:&quot;spin_ccw&quot;}&lt;/code&gt;, the web bridge parses and validates the JSON payload. Malformed or unsupported payloads are rejected, and valid payloads are forwarded to the motor command topic (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rover/motor/cmd&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The web bridge also subscribes to the sonar sample topic (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rover/sonar/sample&lt;/code&gt;). When an ultrasonic reading is received, the web bridge publishes it to every connected WebSocket client.&lt;/p&gt;

&lt;h3 id=&quot;gamepad-support&quot;&gt;Gamepad Support&lt;/h3&gt;

&lt;p&gt;The web UI can also translate browser gamepad input into the same command messages used by the on-screen controls. This keeps the backend unchanged: whether a command comes from a button click, a keyboard shortcut, or a gamepad axis, the WebSocket server still receives JSON, validates it, and forwards it to MQTT.&lt;/p&gt;

&lt;p&gt;For testing I used an Xbox 360 Gamepad, connected via USB:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/robotics-in-go-building-a-raspberry-pi-rover/xbox-gamepad.png&quot; alt=&quot;xbox gamepad&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h2&gt;

&lt;p&gt;The next challenge in this project is to delegate reading from ultrasonic sensors to a separate microcontroller. I will be using an STM32F407 Discovery Board, a common microcontroller development board based on the Arm Cortex-M4 processor. This will be used to send distance readings back to the Raspberry Pi via a USB UART connection.&lt;/p&gt;

&lt;p&gt;Stay tuned for the next post, where we’ll use TinyGo to compile and run Go binaries on the STM32.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;p&gt;Libraries and frameworks:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://gobot.io/&quot;&gt;Gobot website&lt;/a&gt; - A framework for robots, drones, and the Internet of Things&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://periph.io/&quot;&gt;Periph website&lt;/a&gt; - A standalone hardware library with limited external dependencies&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/eclipse-paho/paho.mqtt.golang&quot;&gt;Eclipse Paho MQTT Go client&lt;/a&gt; - Connect to an MQTT message broker from Go&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Videos:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=CAvawEcxoPU&quot;&gt;Understanding I2C&lt;/a&gt; - Great ~10 minute video by &lt;a href=&quot;https://www.youtube.com/@Rohde-Schwarz&quot;&gt;Rohde &amp;amp; Schwarz&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Embedded Go:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://embeddedgo.github.io/&quot;&gt;Embedded Go&lt;/a&gt; - Bare-metal programming with Go&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/embeddedgo/stm32&quot;&gt;Embedded Go STM32&lt;/a&gt; - Support for the STM32 microcontrollers&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://tinygo.org/&quot;&gt;TinyGo&lt;/a&gt; - Bring Go to embedded systems and to the modern web using a compiler based on LLVM&lt;/li&gt;
&lt;/ul&gt;
</description>
					<pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate>
					<link>https://tristanpenman.com/blog/posts/2026/05/22/robotics-in-go-building-a-raspberry-pi-rover/</link>
					<guid isPermaLink="true">https://tristanpenman.com/blog/posts/2026/05/22/robotics-in-go-building-a-raspberry-pi-rover/</guid>
				</item>
			
		
			
				<item>
					<title>Machine Translation at the Edge: Part 2 </title>
					
					<description>&lt;p&gt;In the &lt;a href=&quot;https://tristanpenman.com/blog/posts/2026/01/05/machine-translation-at-the-edge-part-1/&quot;&gt;previous post&lt;/a&gt;, we introduced the machine translation problem and worked through the fundamentals of the MarianNMT framework. For this installment, we’ve set ourselves a goal to port Marian to the Rockchip NPU. More precisely, we want to download a pretrained &lt;em&gt;MarianMT&lt;/em&gt; model from Hugging Face, convert it to Rockchip’s RKNN format, then perform inference directly on the Rockchip NPU - all using Python!&lt;/p&gt;

&lt;p&gt;Throughout this journey, we discuss issues such as quantization, input padding and layer compatibility. For those wanting more detail, the complete source code for this project can be found &lt;a href=&quot;https://github.com/tristanpenman/marian-rknn&quot;&gt;on GitHub&lt;/a&gt;. &lt;em&gt;This includes a native C++ implementation too!&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;contents&quot;&gt;Contents&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#part-1-recap&quot;&gt;Part 1 Recap&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#mariannmt&quot;&gt;MarianNMT&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#hugging-face&quot;&gt;Hugging Face&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#hugging-face-transformers&quot;&gt;Hugging Face Transformers&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#mariannmt-vs-marianmt&quot;&gt;MarianNMT vs MarianMT&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#preflight&quot;&gt;Preflight&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#dependencies&quot;&gt;Dependencies&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#sentencepiece&quot;&gt;SentencePiece&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#preflight-script&quot;&gt;Preflight Script&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#pytorch-vs-onnx&quot;&gt;PyTorch vs ONNX&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#converting-to-onnx&quot;&gt;Converting to ONNX&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#marian-onnx-converter&quot;&gt;Marian-ONNX-Converter&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#model-files&quot;&gt;Model Files&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#converting-to-rknn&quot;&gt;Converting to RKNN&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#quantization&quot;&gt;Quantization&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#dynamic-ops&quot;&gt;Dynamic Ops&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#inference-engine&quot;&gt;Inference Engine&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#initialisation&quot;&gt;Initialisation&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#tokenization&quot;&gt;Tokenization&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#encoder&quot;&gt;Encoder&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#decoder-loop&quot;&gt;Decoder Loop&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#detokenization&quot;&gt;Detokenization&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#decoding-strategies&quot;&gt;Decoding Strategies&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#greedy-decoding&quot;&gt;Greedy Decoding&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#beam-search&quot;&gt;Beam Search&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#benchmarks&quot;&gt;Benchmarks&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#sample-text&quot;&gt;Sample Text&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#benchmark-script&quot;&gt;Benchmark Script&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#results&quot;&gt;Results&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#wrap-up&quot;&gt;Wrap Up&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#references&quot;&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;part-1-recap&quot;&gt;Part 1 Recap&lt;/h2&gt;

&lt;p&gt;When it comes to Machine Translation, there are several well-established models we can choose from. After exploring a few options, we ultimately decided to focus on &lt;em&gt;Marian&lt;/em&gt; Transformer-based models. These models have seen adoption in industry and are available for a wide range of language pairs. Marian is also simple enough that if we wanted to support transformer-based models trained using another toolkit, such as &lt;a href=&quot;https://github.com/awslabs/sockeye&quot;&gt;Sockeye&lt;/a&gt; or &lt;a href=&quot;https://opennmt.net/&quot;&gt;OpenNMT&lt;/a&gt;, many of the same techniques would apply.&lt;/p&gt;

&lt;p&gt;Let’s visit what “Marian” actually means, starting with MarianNMT.&lt;/p&gt;

&lt;p&gt;As a quick historical note, Marian NMT was named in honour of Marian Rejewski, a Polish mathematician and cryptologist.&lt;/p&gt;

&lt;h3 id=&quot;mariannmt&quot;&gt;MarianNMT&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://marian-nmt.github.io/&quot;&gt;MarianNMT&lt;/a&gt; is a pure C++ machine learning toolkit, specialised for training Neural Machine Translation (NMT) models. MarianNMT can be compiled to run on the CPU, or with GPU-acceleration using CUDA. Although it can be used to train a range of different machine translation models (e.g. LSTMs and Seq2Seq models) it is most commonly used to train Transformer models.&lt;/p&gt;

&lt;p&gt;MarianNMT includes an efficient inference engine, also written in C++. This is highly portable, but is not written in a way that enables adoption of the RKNN API. If we tried to cross-compile Marian for Rockchip devices, processing would still happen on the CPU, which is not what we want.&lt;/p&gt;

&lt;p&gt;So we’ll need to find another implementation as a reference point. This is where &lt;em&gt;Hugging Face&lt;/em&gt; enters the scene.&lt;/p&gt;

&lt;h3 id=&quot;hugging-face&quot;&gt;Hugging Face&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://huggingface.co/&quot;&gt;Hugging Face&lt;/a&gt; is a platform for sharing machine learning models. It’s an easy way to find and access pretrained models, as well as training and inference pipelines. Models published on Hugging Face typically include a “Model Card” containing useful information about the model, how it was trained, and how to use it. You can think of this like the README file you would find in a GitHub repo.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/machine-translation-at-the-edge-part-2/helsinki-nlp-opus-mt-en-fr.png&quot; alt=&quot;helsinki-nlp/opus-mt-en-fr&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;There are a range of Marian-compatible models on Hugging Face. As with the first post, we will focus on an OPUS MT English-to-French model known as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Helsinki-NLP/opus-mt-en-fr&lt;/code&gt;. This is paired with inference implementation based on Hugging Face Transformers.&lt;/p&gt;

&lt;h3 id=&quot;hugging-face-transformers&quot;&gt;Hugging Face Transformers&lt;/h3&gt;

&lt;p&gt;The &lt;a href=&quot;https://huggingface.co/docs/transformers/en/index&quot;&gt;Hugging Face Transformers&lt;/a&gt; library supports models for text generation, machine translation, summarization and classification (just to name a few). It also provides support for training workflows, model configuration management, and interoperability with backends such as PyTorch and TensorFlow.&lt;/p&gt;

&lt;p&gt;To streamline the experience of using a model, Hugging Face provides instructions for various Cloud AI providers and on your own system. Getting started with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Helsinki-NLP/opus-mt-en-fr&lt;/code&gt; only requires a few lines of code:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/machine-translation-at-the-edge-part-2/model-instructions.png&quot; alt=&quot;Model Instructions&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Hugging Face Transformers has a built-in Marian implementation, which makes getting started very easy!&lt;/p&gt;

&lt;h3 id=&quot;mariannmt-vs-marianmt&quot;&gt;MarianNMT vs MarianMT&lt;/h3&gt;

&lt;p&gt;The last thing to note is the difference between MarianNMT and MarianMT, which are easily confused…&lt;/p&gt;

&lt;p&gt;When we say &lt;em&gt;MarianNMT&lt;/em&gt;, we’re referring to the C++ toolkit introduced above. When we say &lt;em&gt;MarianMT&lt;/em&gt; we’re referring to a Marian implementation based on &lt;em&gt;Hugging Face Transformers&lt;/em&gt;. It comes with a set of pretrained models, and this is what we’ll use as the basis for our Rockchip NPU implementation.&lt;/p&gt;

&lt;h2 id=&quot;preflight&quot;&gt;Preflight&lt;/h2&gt;

&lt;p&gt;Let’s get MarianMT up and running, so that we have a baseline for the rest of this project. &lt;em&gt;Note: Everything you see below should be executed on a PC.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first thing we’ll need to do is install the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;transformers&lt;/code&gt; library, and some of its dependencies:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pip &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  sentencepiece &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  torch &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  transformers
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This command also installs compatible versions of PyTorch and &lt;a href=&quot;https://github.com/google/sentencepiece&quot;&gt;SentencePiece&lt;/a&gt;, which are dependencies for MarianMT.&lt;/p&gt;

&lt;h3 id=&quot;sentencepiece&quot;&gt;SentencePiece&lt;/h3&gt;

&lt;p&gt;You may have noticed on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Helsinki-NLP/opus-mt-en-fr&lt;/code&gt; model card earlier, that pre-processing includes “normalization + sentencepiece”. This means we’ll need to use Google’s &lt;a href=&quot;https://github.com/google/sentencepiece&quot;&gt;SentencePiece&lt;/a&gt; library to tokenize input.&lt;/p&gt;

&lt;p&gt;SentencePiece is an unsupervised text tokenizer and detokenizer, based on the byte-pair-encoding (BPE) and unigram algorithms. It is responsible for splitting a source text into subwords. We also use it to convert model output into human-readable sentences.&lt;/p&gt;

&lt;h3 id=&quot;test-script&quot;&gt;Test Script&lt;/h3&gt;

&lt;p&gt;We can put this all together using a simple Python script:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformers&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MarianMTModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MarianTokenizer&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;device&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;device&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;cuda&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cuda&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_available&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;cpu&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Using device: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;device&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;model_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Helsinki-NLP/opus-mt-en-fr&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MarianTokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from_pretrained&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MarianMTModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from_pretrained&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;device&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Hello, I am a fish. What kind of animal are you?&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;inputs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;return_tensors&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;pt&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;device&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;translated&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;translation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tokenizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;skip_special_tokens&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;translated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;translation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We’ve deviated from the model card a bit here, opting to use Marian-specific classes instead of relying on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AutoTokenizer&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AutoModelForSeq2SeqLM&lt;/code&gt;. This is just for clarity.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MarianTokenizer&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MarianMTModel&lt;/code&gt; classes know how to tokenise input and transform, respectively. The tokeniser is also used to convert tokens generated by the model into human-readable output. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MarianTokenizer&lt;/code&gt; does this using SentencePiece behind the scenes.&lt;/p&gt;

&lt;p&gt;When we run the test script above, we see that it successfully translates the English phrase “Hello, I am a fish. What kind of animal are you?” into French:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;python test.py
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Bonjour, je suis un poisson, quel genre d&apos;animal êtes-vous ?&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/tristanpenman/marian-rknn&quot;&gt;marian-rknn&lt;/a&gt; repo contains a more robust version of this script, called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preflight.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: The first time you run this script it will download the necessary pretrained model files. The model will be stored in a local cache, so that subsequent invocations don’t require another download.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;pytorch-vs-onnx&quot;&gt;PyTorch vs ONNX&lt;/h3&gt;

&lt;p&gt;The Hugging Face model downloaded above will be in PyTorch format, since that’s what Hugging Face Transformers uses internally. The question we need to answer is how to go from PyTorch format to RKNN format.&lt;/p&gt;

&lt;p&gt;The key challenge is that RKNN Toolkit cannot just run or convert a PyTorch model by executing arbitrary Python code. We have two paths we can follow. The first uses TorchScript, which is a way to serialize and save models to disk so they can be used in alternative runtime environments (e.g. C++). Unfortunately, TorchScript has been deprecated since PyTorch 2.9.&lt;/p&gt;

&lt;p&gt;Our second choice is ONNX - the &lt;a href=&quot;https://onnx.ai/onnx/intro/&quot;&gt;Open Neural Network Exchange&lt;/a&gt; format. Once a model has been converted to ONNX, it is relatively easy to convert it to RKNN format. The ONNX runtime also supports the concept of &lt;a href=&quot;https://onnxruntime.ai/docs/execution-providers/&quot;&gt;Runtime Execution Providers&lt;/a&gt;, making it possible to run a model on a wide range of hardware platforms.&lt;/p&gt;

&lt;h2 id=&quot;converting-to-onnx&quot;&gt;Converting to ONNX&lt;/h2&gt;

&lt;p&gt;With a little bit of research, we find that PyTorch directly handles ONNX exports. This seems relatively simple at first:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;transformers&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MarianMTModel&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MarianMTModel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from_pretrained&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Helsinki-NLP/opus-mt-en-de&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;dummy_input&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;randint&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# token IDs
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;onnx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;export&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dummy_input&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,),&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;marian.onnx&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;opset_version&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;13&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This would work well if you wanted to run the model on a GPU. In practice there’s more work required to export the model in a way that will cleanly convert to RKNN format and run on the Rockchip NPU. Thankfully, &lt;a href=&quot;https://github.com/kcosta42&quot;&gt;Kevin Costa’s&lt;/a&gt; Marian-ONNX-Converter project will save us a great deal of time and effort!&lt;/p&gt;

&lt;h3 id=&quot;marian-onnx-converter&quot;&gt;Marian-ONNX-Converter&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/kcosta42/Marian-ONNX-Converter&quot;&gt;Marian-ONNX-Converter&lt;/a&gt; does a few things that will be helpful for us. It exports a MarianMT model into a split encoder/decoder ONNX layout, and preserves the extra artifacts that are required to invoke the model outside of MarianMT. It also provides an ONNX implementation which can run on the CPU, or on a GPU using CUDA.&lt;/p&gt;

&lt;p&gt;Marian-ONNX-Converter also happens to support quantization, however we disable quantization when converting from PyTorch to ONNX, as this would introduce layers into the model that are not supported by the RKNN API.&lt;/p&gt;

&lt;p&gt;To convert the PyTorch model we’ll first need to find the cached model path, which was downloaded during ‘Preflight’:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;MODEL_PATH&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;python &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;from huggingface_hub import snapshot_download; print(snapshot_download(&apos;Helsinki-NLP/opus-mt-en-fr&apos;))&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can then convert a model from PyTorch to ONNX:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python Marian-ONNX-Converter/convert.py &lt;span class=&quot;nv&quot;&gt;$MODEL_PATH&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--no-quantize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We have to explicitly choose no quantization at this step, otherwise Marian-ONNX-Converter will attempt to quantize the encoder and decoder models in ways that are not supported by RKNN. It would also be premature to quantize the model before establishing baseline performance metrics. &lt;em&gt;This will be the topic of part 3.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: I’m currently maintaining a fork of Marian-ONNX-Converter, which includes various small fixes that make it more appropriate for this project. This can be found &lt;a href=&quot;https://github.com/tristanpenman/Marian-ONNX-Converter&quot;&gt;here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;model-files&quot;&gt;Model Files&lt;/h3&gt;

&lt;p&gt;Once we’ve converted the model to ONNX format, the output can be found in a sub-directory under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;outs/&lt;/code&gt;. In my case this was called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;outs/dd7f6540a7a48a7f4db59e5c0b9c42c8eea67f18&lt;/code&gt;, which was derived from the original model ID.&lt;/p&gt;

&lt;p&gt;The contents of that directory should look something like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;% ls -gho
total 1070376
-rw-r--r--  1    1.5K 13 Feb 21:59 config.json
-rw-r--r--  1    214M 13 Feb 21:59 decoder.onnx
-rw-r--r--  1    190M 13 Feb 21:59 encoder.onnx
-rw-r--r--  1    234K 13 Feb 21:59 lm_bias.bin
-rw-r--r--  1    116M 13 Feb 21:59 lm_weight.bin
-rw-r--r--  1    760K 13 Feb 21:59 source.spm
-rw-r--r--  1     74B 13 Feb 21:59 special_tokens_map.json
-rw-r--r--  1    784K 13 Feb 21:59 target.spm
-rw-r--r--  1    816B 13 Feb 21:59 tokenizer_config.json
-rw-r--r--  1    1.4M 13 Feb 21:59 vocab.json
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;As you might have guessed, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;decoder.onnx&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;encoder.onnx&lt;/code&gt; files are ONNX models. These contain the model graphs and weights for the decoder and encoder, respectively. This is where &lt;a href=&quot;https://en.wikipedia.org/wiki/Attention_(machine_learning)&quot;&gt;attention&lt;/a&gt; happens!&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lm_weight.bin&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lm_bias.bin&lt;/code&gt; files contain the weights and biases for the &lt;em&gt;LM head&lt;/em&gt;. The raw output of a transformer is generally not in a form that is ready for human consumption. The LM head is a feed-forward neural network that converts transformer output into tokens that have meaning for humans.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source.spm&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target.spm&lt;/code&gt; files contain SentencePiece models for tokenization, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vocab.json&lt;/code&gt; file ensures that tokens that occur in both the source and target languages are used efficiently. All other files in this directory are configuration files.&lt;/p&gt;

&lt;h2 id=&quot;converting-to-rknn&quot;&gt;Converting to RKNN&lt;/h2&gt;

&lt;p&gt;Conversion from ONNX to RKNN format is relatively straight-forward, thanks to the RKNN Toolkit. We can simply load an ONNX model, then export that same model to RKNN format using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rknn.api&lt;/code&gt; module:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rknn.api&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RKNN&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RKNN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load_onnx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;model.onnx&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;do_quantization&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;export_rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;model.rknn&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We also want to convert the LM weights and biases from PyTorch format to raw floating point values:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;convert_weights&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Convert Torch LM head tensors to raw float32 for runtime matmul + bias.

    These can be loaded back into tensors in Python, or natively in C++.
    &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;tensor&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;torch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;weights_only&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;weights&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tensor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;detach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;numpy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;astype&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;float32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;weights&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tofile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;output_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;weights&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is not strictly necessary, because our Python code can load the original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lm_bias.bin&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lm_weight.bin&lt;/code&gt; files directly, using PyTorch. However when we port the inference engine to C++, we will need raw floating point values. By converting now, we can be sure that our implementations behave consistently.&lt;/p&gt;

&lt;p&gt;This conversion is handled by the &lt;a href=&quot;https://github.com/tristanpenman/marian-rknn/blob/master/scripts/rknn_convert.py&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rknn_convert.py&lt;/code&gt;&lt;/a&gt; script in my &lt;a href=&quot;https://github.com/tristanpenman/marian-rknn&quot;&gt;marian-rknn&lt;/a&gt; repo.&lt;/p&gt;

&lt;h3 id=&quot;quantization&quot;&gt;Quantization&lt;/h3&gt;

&lt;p&gt;In the example above, we’ve set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;do_quantization=False&lt;/code&gt;, which converts the model graph directly to RKNN format, with no quantization.&lt;/p&gt;

&lt;p&gt;Quantization for encoder/decoder architectures is somewhat involved. For the encoder, we need to build a dataset containing a pair of NumPy files (the input IDs and attention mask) for each example. We then write the file names to a text file, one example per line.&lt;/p&gt;

&lt;p&gt;Here’s what a dataset text file might look like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;calibration/input_ids_1.npy calibration/attention_mask_1.npy
calibration/input_ids_2.npy calibration/attention_mask_2.npy
calibration/input_ids_3.npy calibration/attention_mask_3.npy
calibration/input_ids_4.npy calibration/attention_mask_4.npy
calibration/input_ids_5.npy calibration/attention_mask_5.npy
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The path to this file is then used as an argument for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rknn.build()&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;do_quantization&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;calibration_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Effective quantization requires careful profiling and evaluation of a model, to ensure that performance does not
suffer unnecessarily. I mention this mostly for completeness: for this implementation, we will leave quantization disabled and prioritize a working end-to-end RKNN pipeline.&lt;/p&gt;

&lt;h3 id=&quot;dynamic-ops&quot;&gt;Dynamic Ops&lt;/h3&gt;

&lt;p&gt;The next challenge we have is &lt;em&gt;dynamic inputs&lt;/em&gt;. The Rockchip NPU has limited support for dynamic graph operations, which are required to handle inputs of arbitrary length. To avoid off-loading inference onto the CPU, we need to alter the model to use static input shapes.&lt;/p&gt;

&lt;p&gt;For MarianMT we define these as:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;ENCODER_INPUTS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;input_ids&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;attention_mask&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;ENCODER_INPUT_SIZE_LIST&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;DECODER_INPUTS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;input_ids&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;attention_mask&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;encoder_hidden_states&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;DECODER_INPUT_SIZE_LIST&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;512&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The array &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[1, 32]&lt;/code&gt; specifies an input length of 32 tokens for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input_ids&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attention_mask&lt;/code&gt;. The array &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[1, 32, 512]&lt;/code&gt; specifies an internal state with a shape of 32x512. The value 512 is not arbitrary - it is the size of the embedding used, which corresponds to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;d_model&lt;/code&gt; in the model’s config.json file.&lt;/p&gt;

&lt;p&gt;The constants above are used when exporting the encoder and decoder models:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Load encoder model
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RKNN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;target_platform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;platform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;dynamic_input&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ENCODER_INPUT_SIZE_LIST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load_onnx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoder_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
               &lt;span class=&quot;n&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ENCODER_INPUTS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
               &lt;span class=&quot;n&quot;&gt;input_size_list&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ENCODER_INPUT_SIZE_LIST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Export encoder (trimmed for brevity)
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;export_rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;release&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Load decoder model
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RKNN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;target_platform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;platform&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;dynamic_input&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DECODER_INPUT_SIZE_LIST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load_onnx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decoder_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
               &lt;span class=&quot;n&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DECODER_INPUTS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
               &lt;span class=&quot;n&quot;&gt;input_size_list&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DECODER_INPUT_SIZE_LIST&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Export decoder (trimmed for brevity)
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;export_rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(...)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;release&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;inference-engine&quot;&gt;Inference Engine&lt;/h2&gt;

&lt;p&gt;To keep things simple, our inference engine will be written in Python. Although there are potential benefits to writing an inference engine in C++, starting with Python has three main advantages:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;It will be faster to iterate. We do not need to worry about memory management, linking, and other C++ footguns.&lt;/li&gt;
  &lt;li&gt;Interactions with SentencePiece will be closer to the original Hugging Face implementation, which will be helpful for comparison.&lt;/li&gt;
  &lt;li&gt;Finally, we can take advantage of &lt;em&gt;simulation mode&lt;/em&gt; in the RKNN Toolkit, to run the model on your development machine. This is much faster than transferring .rknn files to device for testing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This section will be divided into five steps, introducing code snippets for each part of the translation process:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Initialisation&lt;/li&gt;
  &lt;li&gt;Tokenization&lt;/li&gt;
  &lt;li&gt;Encoder&lt;/li&gt;
  &lt;li&gt;Decoder Loop&lt;/li&gt;
  &lt;li&gt;Detokenization&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s dive in!&lt;/p&gt;

&lt;h3 id=&quot;initialisation&quot;&gt;Initialisation&lt;/h3&gt;

&lt;p&gt;During initialisation, we have to load each of the components of the model. This begins with two SentencePiece tokenisation models - one for the source language, and another for the target language:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# source language
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spm_src&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SentencePieceProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model_file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input_path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/source.spm&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# target language
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;spm_tgt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SentencePieceProcessor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model_file&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input_path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/target.spm&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We also need to load a vocab file, which maps token substrings to integers:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# load vocab from json file
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vocab&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vocab_inv&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;load_vocab&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;input_path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/vocab.json&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vocab_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Finally, we need to load the encoder and decoder RKNN models using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RKNNLite&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# load encoder
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rknn_enc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RKNNLite&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn_enc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load_rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model_path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/encoder.rknn&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# load decoder
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rknn_dec&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RKNNLite&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn_dec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load_rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model_path&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/decoder.rknn&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;tokenization&quot;&gt;Tokenization&lt;/h3&gt;

&lt;p&gt;You may wonder why we need two tokenizers &lt;em&gt;and&lt;/em&gt; a vocabulary file. Surely a tokeniser would be sufficient for converting an input string into a series of integer token IDs…&lt;/p&gt;

&lt;p&gt;There is a good reason for this. Marian allows the source and target languages to use the same token ID space. Say you have a token that appears in both the source and target languages (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;th&lt;/code&gt;), it can be more efficient to train the model using the same token ID for both languages.&lt;/p&gt;

&lt;p&gt;The problem is that the two SentencePiece models are trained independently, leading common tokens to have different token IDs. The purpose of the vocab file is to correct this, by mapping token substrings to a common token ID space. This is best illustrated with a code snippet:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# break input string into token substrings
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pieces&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spm_src&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out_type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# map token substrings to token IDs
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tokens&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vocab&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;piece&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unk_token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;piece&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pieces&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;These token IDs can then be used as input to the encoder.&lt;/p&gt;

&lt;h3 id=&quot;encoder&quot;&gt;Encoder&lt;/h3&gt;

&lt;p&gt;The encoder begins by building a fixed-length &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input_ids&lt;/code&gt; tensor. This is padded or truncated to the appropriate input length. The “end of stream” and “pad” tokens are part of the model configuration. We also need to build an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attention_mask&lt;/code&gt;, in which valid token positions are marked as 1 and padded positions as 0. This gives the model visibility into which positions are meaningful and which are merely padding.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# prepare encoder inputs and attention mask
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoder_input_ids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;prepare_encoder_inputs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tokens&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;enc_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pad_token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eos_token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;attention_mask&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;build_attention_mask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoder_input_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eos_token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To invoke the encoder, we call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rknn_enc.inference(...)&lt;/code&gt; with both arrays in the order expected by the model:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# invoke the encoder
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoder_outputs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rknn_enc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;inference&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encoder_input_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attention_mask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;encoder_hidden_states&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;encoder_outputs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The return value is a tensor (named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;encoder_hidden_states&lt;/code&gt;) which contains an embedded representation of each source token position. This will be used by the decoder to extract meaning from the source text.&lt;/p&gt;

&lt;h3 id=&quot;decoder-loop&quot;&gt;Decoder Loop&lt;/h3&gt;

&lt;p&gt;Unlike the encoder, which runs just once, the decoder runs in an &lt;em&gt;autoregressive&lt;/em&gt; loop. This means that each token generated depends on the output from previous iterations of the decoder.&lt;/p&gt;

&lt;p&gt;The decoder requires three input tensors:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Decoder input IDs (not the same as the input IDs passed to the encoder)&lt;/li&gt;
  &lt;li&gt;The same attention mask prepared earlier&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;encoder_hidden_states&lt;/code&gt; produced by the encoder&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first input is essentially the decoder’s “working memory”. On each iteration of the decoder, the model may attend to tokens that were previously generated. This helps inform which token is chosen next.&lt;/p&gt;

&lt;p&gt;The third input is simply the output of the encoder.&lt;/p&gt;

&lt;p&gt;We have a choice of two different decoding strategies, which are discussed further below:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;beam_search&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# simpler, but input may be of slightly lower quality
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;output_tokens&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;greedy_decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;rknn_dec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;attention_mask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;encoder_hidden_states&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# more complex, but better output quality
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;beam_width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;beam_depth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;beam_search&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;output_tokens&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;beam_search_decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;rknn_dec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;attention_mask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;encoder_hidden_states&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Greedy decoding is simpler, so we’ll focus on that for now. This snippet of code shows the entire decoder loop for greedy decoding:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;greedy_decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;rknn_dec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;attention_mask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;encoder_hidden_states&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;weight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;bias&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;decoder_start_token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;pad_token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;eos_token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;dec_len&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# prepare autoregressive decoder input; start by fully padding the tensor
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;decoder_input_ids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;full&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dec_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pad_token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dtype&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;decoder_input_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;decoder_start_token_id&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# this will hold our final decoder output
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;output_tokens&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# main loop
&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dec_len&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# invoke decoder, passing in the three inputs
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;outputs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rknn_dec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;inference&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;inputs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decoder_input_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;attention_mask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;encoder_hidden_states&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# apply LM head, which converts the output into logits
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;decoder_output&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;outputs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;hidden&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;decoder_output&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;astype&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;float32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;logits&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hidden&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;weight&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bias&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# greedily decode next token by choosing the candidate
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;# with the highest logit
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;next_token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;argmax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;output_tokens&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;next_token&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# check for end of output
&lt;/span&gt;        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;next_token&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eos_token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;# otherwise, append token to decoder input for next iteration
&lt;/span&gt;        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dec_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;decoder_input_ids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;next_token&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output_tokens&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The output of the decoder is a sequence of token IDs. The final step is to turn those token IDs into human-readable text!&lt;/p&gt;

&lt;h3 id=&quot;detokenization&quot;&gt;Detokenization&lt;/h3&gt;

&lt;p&gt;Detokenization begins by inverting the vocab mapping applied earlier - converting token IDs into substrings. Those substrings are passed to the target tokenizer to be converted into human-readable text:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;decoded_pieces&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token_id&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output_tokens&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token_id&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;eos_token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pad_token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;piece&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vocab_inv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;token_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;piece&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;decoded_pieces&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;piece&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;spm_tgt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;decode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;decoded_pieces&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;decoding-strategies&quot;&gt;Decoding Strategies&lt;/h2&gt;

&lt;p&gt;In the decoder loop section above, we mentioned having two decoding strategies to choose from. These are Greedy Decoding and Beam Search. This choice is all about trade-offs.&lt;/p&gt;

&lt;p&gt;Greedy decoding is fast and deterministic, picking the highest probability token at each step. Beam search explores multiple hypotheses in parallel, which often improves fluency or adequacy. However it also increases compute, memory pressure, and book-keeping costs.&lt;/p&gt;

&lt;h3 id=&quot;greedy-decoding&quot;&gt;Greedy Decoding&lt;/h3&gt;

&lt;p&gt;As tokens are generated, greedy decoding uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;np.argmax(logits)&lt;/code&gt; to choose the highest probability token to append to the output.&lt;/p&gt;

&lt;p&gt;An interesting observation we can make here is that &lt;em&gt;logits&lt;/em&gt; are not the same as probabilities. Normally, we would use a Softmax function to convert the logits to probabilities:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# calculate softmax
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logits&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;exp_logits&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;exp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;probs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;exp_logits&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exp_logits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In the case of greedy decoding, we can rely on the fact that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;argmax(logits)&lt;/code&gt; will identify the same token as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;argmax(probs)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Although efficient, the downside of greedy search is that the decoder cannot backtrack on tokens that lead to poorer translations.&lt;/p&gt;

&lt;h3 id=&quot;beam-search&quot;&gt;Beam Search&lt;/h3&gt;

&lt;p&gt;Beam search addresses this by managing multiple candidate sequences at each step. The number of concurrent sequences (or beams) is known as the beam width.&lt;/p&gt;

&lt;p&gt;To decide which beams should be extended, and which should be culled, the algorithm maintains a cumulative probability. After expanding each candidate beam with possible next tokens, only the top-ranked hypotheses are preserved.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/machine-translation-at-the-edge-part-2/beam-search.png&quot; alt=&quot;Beam Search&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: &lt;a href=&quot;https://d2l.ai/chapter_recurrent-modern/beam-search.html&quot;&gt;Chapter 10.8 - Beam Search&lt;/a&gt; from &lt;a href=&quot;https://d2l.ai/index.html&quot;&gt;Dive Into Deep Learning&lt;/a&gt;.&lt;br /&gt;This diagram shows a beam size of 2 and maximum sequence length of 3. Greedy search can be treated as a special case of Beam Search where the beam size is set to 1.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Beam search reduces the risk of early mistakes dominating the final output. But it comes at the cost of compute, memory pressure and additional book-keeping. This extra work is primarily in the decoder and LM-head, which must be executed &lt;em&gt;once per beam&lt;/em&gt; on each iteration.&lt;/p&gt;

&lt;h2 id=&quot;benchmarks&quot;&gt;Benchmarks&lt;/h2&gt;

&lt;p&gt;The final step in this process is benchmarking. How many sentences can the RK3588 translate per minute? Or more precisely, &lt;em&gt;how many tokens?&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;sample-text&quot;&gt;Sample Text&lt;/h3&gt;

&lt;p&gt;To prepare for the benchmark, we need a file containing input sentences that are representative of what our system will encounter under real world conditions. The &lt;em&gt;Random English Sentences&lt;/em&gt; dataset from &lt;a href=&quot;https://www.kaggle.com/datasets/dalageo/random-english-sentences&quot;&gt;Kaggle&lt;/a&gt; is a good place to start! Here’s what the first 10 lines look like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;I didn&apos;t know what to wear to my best friend&apos;s funeral.
There used to be a big cherry tree behind my house.
Sometimes, books and movies made her feel things she couldn&apos;t explain.
My bandaid wasn&apos;t sticky anymore so it fell off on the way to school.
He only had one drink but he&apos;s completely wasted.
In Hong Kong, everyone lives stacked together.
Her parents had been separated for almost three years, but she never expected them to actually go through with the divorce.
These are the things that I want from a relationship.
When one considers the issue from a different perspective, the answer seems to change.
She twisted her ankle and couldn&apos;t go to soccer anymore.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;benchmark-script&quot;&gt;Benchmark Script&lt;/h3&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;marian-rknn&lt;/code&gt; repo includes a benchmark script (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;benchmark.py&lt;/code&gt;) which repeatedly translates input for a fixed period of time. Once complete, it prints throughput and token count statistics.&lt;/p&gt;

&lt;p&gt;To run the benchmark, we have to provide a model path, a dataset, and a maximum runtime (in seconds). The benchmark will iterate over the dataset until the time budget is reached, then report aggregate throughput and per-stage timings:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python scripts/benchmark.py &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  outs/dd7f6540a7a48a7f4db59e5c0b9c42c8eea67f18 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  datasets/en-phrases.txt &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  120 &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; results-greedy.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can run the same test using Beam Search:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python scripts/benchmark.py &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--beam-search&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--beam-width&lt;/span&gt; 3 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  outs/dd7f6540a7a48a7f4db59e5c0b9c42c8eea67f18 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  datasets/en-phrases.txt &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  120 &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; results-beam-search.txt

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;results&quot;&gt;Results&lt;/h3&gt;

&lt;p&gt;Here are the results!&lt;/p&gt;

&lt;p&gt;As a baseline, greedy decoding with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;datasets/en-phrases.txt&lt;/code&gt; dataset allowed us to produce 2766 output tokens. In total, we processed 207 sentences @ 1.705 sentences per second:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Benchmark complete
Elapsed: 121.442 s
Sentences: 207
Sentences/sec: 1.705
Total time: 121295.378 ms
Encoder time: 2492.285 ms
Decoder time: 91355.883 ms
LM head time: 26394.237 ms
Avg total time per sentence: 585.968 ms
Avg encoder time per sentence: 12.040 ms
Avg decoder time per sentence: 441.333 ms
Avg LM head time per sentence: 127.508 ms
Input tokens: 2379
Output tokens: 2766
Decoder iterations: 2766
Input tokens/sec: 19.590
Output tokens/sec: 22.776
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When we run the same test with beam search enabled, and a beam width of 3, we see how much of an impact it can have…&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Benchmark complete
Elapsed: 121.982 s
Sentences: 52
Sentences/sec: 0.426
Total time: 121851.846 ms
Encoder time: 631.315 ms
Decoder time: 72169.512 ms
LM head time: 20800.915 ms
Avg total time per sentence: 2343.305 ms
Avg encoder time per sentence: 12.141 ms
Avg decoder time per sentence: 1387.875 ms
Avg LM head time per sentence: 400.018 ms
Input tokens: 663
Output tokens: 729
Decoder iterations: 2177
Input tokens/sec: 5.435
Output tokens/sec: 5.976
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Average decoder time per sentence goes up considerably, and the number of output tokens produced drops from 2766 to 729. Overall, we processed 52 sentences @ 0.426 sentences per second.&lt;/p&gt;

&lt;p&gt;It’s worth noting that this is without &lt;em&gt;any&lt;/em&gt; attempt at further optimisation.&lt;/p&gt;

&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap Up&lt;/h2&gt;

&lt;p&gt;At this point, we have a complete path from a pretrained MarianMT model to inference on the Rockchip NPU: exporting to ONNX, converting the encoder and decoder to RKNN, running tokenization and detokenization, and comparing greedy decoding with beam search.&lt;/p&gt;

&lt;p&gt;There is still plenty of room for refinement. Quantization can be used to reduce memory usage and compute overhead, while further optimisation of the decoder loop and LM head may improve throughput. If you want to follow future progress, keep an eye on the &lt;a href=&quot;https://github.com/tristanpenman/marian-rknn&quot;&gt;marian-rknn&lt;/a&gt; repo we’ve used throughout this post, where I will continue to document quantisation and optimisation efforts.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://colabdoge.medium.com/understanding-sentencepiece-under-standing-sentence-piece-ac8da59f6b08&quot;&gt;Understanding SentencePiece&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://clehaxze.tw/gemlog/2023/12-18-using-dynamic-input-shapes-on-rknn-rk3588.gmi&quot;&gt;Using dynamic input shapes on RKNN/RK3588&lt;/a&gt; by Martin Chang&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/kcosta42/Marian-ONNX-Converter&quot;&gt;Marian-ONNX&lt;/a&gt; by &lt;a href=&quot;https://github.com/kcosta42&quot;&gt;Kevin Costa&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://medium.com/@johnos3747/unlocking-on-device-ai-ee488cd3eb00&quot;&gt;Unlocking On-Device AI&lt;/a&gt; by &lt;a href=&quot;https://medium.com/@johnos3747&quot;&gt;John OSullivan&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/rockchip-linux/rknpu2/tree/master/examples/rknn_api_demo&quot;&gt;RKNN API Demo&lt;/a&gt; by Rockchip&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html&quot;&gt;Quantize ONNX Models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
					<pubDate>Fri, 27 Feb 2026 00:00:00 +0000</pubDate>
					<link>https://tristanpenman.com/blog/posts/2026/02/27/machine-translation-at-the-edge-part-2/</link>
					<guid isPermaLink="true">https://tristanpenman.com/blog/posts/2026/02/27/machine-translation-at-the-edge-part-2/</guid>
				</item>
			
		
			
				<item>
					<title>Machine Translation at the Edge: Part 1 </title>
					
					<description>&lt;p&gt;This is my third post exploring Edge AI, with a focus on the Rockchip RK3588 NPU. This post dives into Machine Translation, a Natural Language Processing task that has traditionally been associated with Cloud AI services. With advances in Edge AI hardware, model architectures and distillation techniques, it is now feasible to perform Neural Machine Translation directly on an embedded device.&lt;/p&gt;

&lt;p&gt;The goal of this post (and the next) is to build a Neural Machine Translation pipeline for the Rockchip NPU. We’ll walk through model selection, quantization, and deployment using the RKNN Toolkit. Python and C++ source code will be available on GitHub. The initial implementation is based on the &lt;a href=&quot;https://www.khadas.com/edge2&quot;&gt;Khadas Edge2&lt;/a&gt; development board, but should be easy to port to other RKNN-based devices.&lt;/p&gt;

&lt;p&gt;If you haven’t already, I recommend reading the &lt;a href=&quot;https://tristanpenman.com/blog/posts/2025/07/20/edge-ai-using-the-rockchip-npu/&quot;&gt;first&lt;/a&gt; and &lt;a href=&quot;https://tristanpenman.com/blog/posts/2025/12/07/tiny-language-models/&quot;&gt;second&lt;/a&gt; posts, as they cover helpful background material.&lt;/p&gt;

&lt;h3 id=&quot;contents&quot;&gt;Contents&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#machine-translation&quot;&gt;Machine Translation&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#statistical-models&quot;&gt;Statistical Models&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#encoder-decoder-models&quot;&gt;Encoder-Decoder Models&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#rnns-and-lstms&quot;&gt;RNNs and LSTMs&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#transformers&quot;&gt;Transformers&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#rknn-platform&quot;&gt;RKNN Platform&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#why-use-the-edge2&quot;&gt;Why use the Edge2?&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#platform-limitations&quot;&gt;Platform Limitations&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#rknn-toolkit&quot;&gt;RKNN Toolkit&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#model-support&quot;&gt;Model Support&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#rknn-model-zoo&quot;&gt;RKNN Model Zoo&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#lite-transformer&quot;&gt;Lite Transformer&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#long-short-range-attention&quot;&gt;Long-Short Range Attention&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#other-choices&quot;&gt;Other Choices&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#mariannmt-vs-marianmt&quot;&gt;MarianNMT vs MarianMT&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#build-from-source-optional&quot;&gt;Build from Source (Optional)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#npz-format&quot;&gt;NPZ Format&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#pretrained-models&quot;&gt;Pretrained Models&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#test-flight&quot;&gt;Test Flight&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#getting-the-model&quot;&gt;Getting the Model&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#tokenization&quot;&gt;Tokenization&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#vocab-file&quot;&gt;Vocab File&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#full-pipeline&quot;&gt;Full Pipeline&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#next-steps&quot;&gt;Next Steps&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#references&quot;&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;machine-translation&quot;&gt;Machine Translation&lt;/h2&gt;

&lt;p&gt;Machine Translation is a technique used to automatically, or mechanically, translate text (or speech) from one language to another. We generally refer to the two languages involved as the ‘source’ and ‘target’ languages. At a high level, the machine reads text in the source language, and generates a corresponding translation in the target language.&lt;/p&gt;

&lt;p&gt;Machine Translation has been an active area of research since the early 1950s. In fact, one of the first conferences on &lt;em&gt;Mechanical Translation&lt;/em&gt; (as it was known at the time) was hosted at MIT in 1952. With such a rich history, there are &lt;em&gt;many&lt;/em&gt; translation models to choose from. We’ll briefly review some earlier architectures here.&lt;/p&gt;

&lt;h3 id=&quot;statistical-models&quot;&gt;Statistical Models&lt;/h3&gt;

&lt;p&gt;Human language is highly redundant. After seeing the first few letters of a word we can often predict the following letters with fairly high accuracy. The same is true for words in a sentence, and even phrases. This motivates the treatment of language as &lt;em&gt;probabilistic&lt;/em&gt; in nature.&lt;/p&gt;

&lt;p&gt;In &lt;em&gt;Statistical Machine Translation&lt;/em&gt; (SMT), a model learns a probabilistic mapping from a source language to a target language. A classic example of this is Phrase-Based Machine Translation (PBMT). A sentence is first segmented into phrases, and each phrase is mapped to corresponding phrases in the target language:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/machine-translation-at-the-edge-part-1/word-alignment.png&quot; alt=&quot;Word Alignment&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image credit: Adapted from the original image found &lt;a href=&quot;https://www.statmt.org/wpt05/mt-shared-task/&quot;&gt;here&lt;/a&gt;.&lt;br /&gt;This was included in the &lt;em&gt;ACL 2005 Workshop on
Building and Using Parallel Texts&lt;/em&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Once candidate word/phrase matches have been selected, the system searches for the best overall translation by ranking and re-ordering the translations, before combining them to produce a fluent result.&lt;/p&gt;

&lt;h4 id=&quot;aside-moses&quot;&gt;Aside: Moses&lt;/h4&gt;

&lt;p&gt;If you start digging into the literature around statistical models, you are likely to stumble upon &lt;a href=&quot;https://www2.statmt.org/moses/&quot;&gt;Moses&lt;/a&gt;, an open-source SMT system. Although Moses is no longer common in front-line translation systems, it is still relevant for ‘low-resource’ language research (i.e. translating languages with limited training data) and as a teaching tool.&lt;/p&gt;

&lt;p&gt;You may also discover &lt;a href=&quot;https://pypi.org/project/sacremoses/&quot;&gt;Sacremoses&lt;/a&gt;, a Python port of Moses’ text preprocessing scripts. This is often used in older model pipelines, or as an optional dependency. SentencePiece, BPE and Unigram tokenizers are more common these days.&lt;/p&gt;

&lt;p&gt;The primary issue with SMT systems is that they depend on a pipeline of hand-crafted components. Because translation occurs across multiple stages (e.g. segmentation, alignment, phrase tables, and reordering) errors in early stages can easily propagate to the final translation. The result ends up being brittle and expensive to maintain.&lt;/p&gt;

&lt;h3 id=&quot;encoder-decoder-models&quot;&gt;Encoder-Decoder Models&lt;/h3&gt;

&lt;p&gt;Encoder-Decoder models replace the individual components of SMT and PBMT with a single end-to-end model that can learn directly from a large collection of source-target language pairs. This works remarkably well on high-resource languages (i.e. those with plenty of high-quality training data).&lt;/p&gt;

&lt;p&gt;Another reason that Encoder-Decoder architectures have been so successful is that Machine Translation is fundamentally a sequence-to-sequence problem - you take a variable-length source sentence and produce a target sentence. The source and target sentences will often be different lengths, since an expression in one language may look very different in another.&lt;/p&gt;

&lt;p&gt;Even within the sub-category of Encoder-Decoder models, we have a few variations to consider… the first are Recurrent Neural Networks.&lt;/p&gt;

&lt;h3 id=&quot;rnns-and-lstms&quot;&gt;RNNs and LSTMs&lt;/h3&gt;

&lt;p&gt;Recurrent Neural Networks (RNNs) are designed for processing sequential data, where the order of elements is important. This is clearly the case for language, where the meaning of a word depends on the presence and order of words (or more generally, tokens) around it.&lt;/p&gt;

&lt;p&gt;Unlike feed-forward networks (FFNs), which process inputs independently, RNNs utilize &lt;em&gt;recurrent&lt;/em&gt; connections. These recurrent connections carry ‘hidden state’ from one iteration to the next - allowing the network to progressively capture the meaning in a sentence. This is well suited to translation, where the source sentence is first processed by an &lt;em&gt;Encoder RNN&lt;/em&gt; to extract the meaning, then passed to a &lt;em&gt;Decoder RNN&lt;/em&gt; to generate output tokens:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/machine-translation-at-the-edge-part-1/recurrent-encoder-decoder.png&quot; alt=&quot;Recurrent Encoder-Decoder&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Some information is passed on from each iteration of an RNN to the next - this is the hidden state of the RNN. In this architecture, the source sentence is processed by the encoder RNN, until the final token is seen (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;EOS&gt;&amp;lt;/code&amp;gt;, or ‘End of Sequence’). The final hidden state is then used to initialise the decoder RNN, which generates tokens until &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;EOS&gt;&amp;lt;/code&amp;gt;.&amp;lt;/small&amp;gt;&lt;/EOS&gt;&lt;/code&gt;&lt;/EOS&gt;&lt;/code&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;RNNs do suffer from some limitations, such as inherently sequential execution, and difficulty with long-range dependencies. They also have a fixed-sized hidden state vector, which limits representational capacity. Since an entire sentence must be squeezed into a fixed-size vector, RNN-based models may struggle with longer sentences.&lt;/p&gt;

&lt;h3 id=&quot;transformers&quot;&gt;Transformers&lt;/h3&gt;

&lt;p&gt;The most recent Machine Translation models tend to use some variation of the &lt;a href=&quot;https://en.wikipedia.org/wiki/Transformer_(deep_learning)&quot;&gt;Transformer Architecture&lt;/a&gt;, which we covered in the previous post on &lt;a href=&quot;https://tristanpenman.com/blog/posts/2025/12/07/tiny-language-models/&quot;&gt;Tiny Language Models&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/machine-translation-at-the-edge-part-1/transformer-architecture.png&quot; alt=&quot;Transformer Architecture&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: &lt;a href=&quot;https://en.wikipedia.org/wiki/Transformer_(deep_learning_architecture)&quot;&gt;Wikipedia&lt;/a&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Unlike RNNs, Transformers do not rely on recurrent hidden state. They instead use &lt;a href=&quot;https://huggingface.co/blog/Esmail-AGumaan/attention-is-all-you-need#self-attention-mechanism&quot;&gt;Self-attention&lt;/a&gt;, which allows each token to incorporate information from all other tokens in the sequence. This is implemented in such a way that all tokens can be processed in parallel, which addresses one of the major bottlenecks of RNNs.&lt;/p&gt;

&lt;p&gt;After the &lt;em&gt;encoder&lt;/em&gt; has processed a sentence, the &lt;em&gt;decoder&lt;/em&gt; typically generates one token at a time, conditioned on the previously generated tokens.&lt;/p&gt;

&lt;p&gt;Together, these characteristics make the Transformer architecture particularly well suited to Machine Translation, and this is the kind of architecture we’ll be discussing throughout the rest of this exercise.&lt;/p&gt;

&lt;h2 id=&quot;rknn-platform&quot;&gt;RKNN Platform&lt;/h2&gt;

&lt;p&gt;Let’s move on to our target platform - this includes our choice of our hardware and framework support.&lt;/p&gt;

&lt;h3 id=&quot;why-use-the-edge2&quot;&gt;Why use the Edge2?&lt;/h3&gt;

&lt;p&gt;The target for this exercise is the &lt;a href=&quot;https://www.khadas.com/edge2&quot;&gt;Khadas Edge2&lt;/a&gt;. This compact hobbyist board is based on the &lt;a href=&quot;https://www.rock-chips.com/uploads/pdf/2022.8.26/192/RK3588%20Brief%20Datasheet.pdf&quot;&gt;Rockchip RK3588 SoC&lt;/a&gt;, an 8-core 64-bit ARM processor with a dedicated neural processing unit (or NPU).&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/machine-translation-at-the-edge-part-1/khadas-edge2.png&quot; alt=&quot;Khadas Edge2&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The RK3588 SoC uses a Unified Memory Architecture (UMA). In practice, this means the CPU and NPU operate on memory that is allocated from the same pool of system memory. This is different to discrete desktop GPUs, where the CPU and GPU each have their own pool of memory and often results in large data transfers over PCIe.&lt;/p&gt;

&lt;h3 id=&quot;platform-limitations&quot;&gt;Platform Limitations&lt;/h3&gt;

&lt;p&gt;While UMA simplifies communication between the CPU and NPU, we need to be mindful of allocation constraints (e.g. contiguous blocks of memory, required for DMA) and monitor overall memory pressure. Large models can easily lead to out-of-memory errors and allocation failures.&lt;/p&gt;

&lt;p&gt;Bandwidth is also shared, so concurrent CPU and NPU workloads compete for DRAM throughput even if memory capacity is sufficient. Our preference is to find smaller models that are specialised to the task at hand, rather than more generalised large language models.&lt;/p&gt;

&lt;h3 id=&quot;rknn-toolkit&quot;&gt;RKNN Toolkit&lt;/h3&gt;

&lt;p&gt;To take advantage of hardware acceleration on the Rockchip NPU, we need to interface with the RKNPU driver via the RKNN API. Rockchip’s &lt;a href=&quot;https://github.com/airockchip/rknn-toolkit2&quot;&gt;RKNN Toolkit&lt;/a&gt; is an SDK for the Rockchip NPU that supports model conversion in Python (on PC), and inference/evaluation on Rockchip devices.&lt;/p&gt;

&lt;p&gt;The toolkit supports conversion, quantization and fine-tuning models on PC. It also includes runtime and C/C++ headers that we need to interface with the NPU in C++:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/machine-translation-at-the-edge-part-1/rknn-workflow.png&quot; alt=&quot;RKNN Workflow&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;The RKNN Toolkit includes components that run on-device (for inference), and others that are intended to be run on a PC (for model construction and conversion).&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;A typical RKNN workflow begins with a pretrained model in PyTorch, TensorFlow or ONNX format. RKNN Toolkit includes Python APIs to load models from other formats, and export them in RKNN format:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rknn.api&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;RKNN&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RKNN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load_onnx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;model.onnx&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;do_quantization&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dataset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;dataset.txt&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;export_rknn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;model.rknn&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once in RKNN format, a model can be loaded on-device using C++ or Python code.&lt;/p&gt;

&lt;h3 id=&quot;model-support&quot;&gt;Model Support&lt;/h3&gt;

&lt;p&gt;Although there are RKNN implementations for some popular model architectures, most pretrained models will be published in other formats. Common formats in the wild include &lt;a href=&quot;https://pytorch.org/&quot;&gt;PyTorch&lt;/a&gt; and &lt;a href=&quot;https://www.tensorflow.org/&quot;&gt;TensorFlow&lt;/a&gt;. Another common format is ONNX, short for &lt;a href=&quot;https://onnx.ai/&quot;&gt;Open Neural Network Exchange&lt;/a&gt;. This has gained popularity as a model interchange format.&lt;/p&gt;

&lt;p&gt;The RKNN API makes it easy to load and export models, but whether this will be sufficient depends heavily on the model architecture. In some cases, it may be impossible to run a model on the Rockchip NPU due to certain layers or operations not being supported. For example, models that rely on dynamic graph operations are not well supported.&lt;/p&gt;

&lt;p&gt;When converting models, it can be helpful to visualise the model graph, in order to diagnose problems. &lt;a href=&quot;https://github.com/lutzroeder/netron&quot;&gt;Netron&lt;/a&gt; is a convenient and easy-to-use viewer for neural networks:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/machine-translation-at-the-edge-part-1/netron-screenshot.png&quot; alt=&quot;netron screenshot&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image credit: Screenshot taken from the &lt;a href=&quot;https://github.com/lutzroeder/netron&quot;&gt;Netron GitHub repo&lt;/a&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;rknn-model-zoo&quot;&gt;RKNN Model Zoo&lt;/h2&gt;

&lt;p&gt;When implementing a machine learning model, it is generally a bad idea to start from scratch. Let’s look at some of the available examples.&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/airockchip/rknn_model_zoo&quot;&gt;RKNN Model Zoo&lt;/a&gt; provides a range of examples for the Rockchip platform, primarily computer vision models. But there is one we can use for Machine Translation, based on a model architecture known as the Lite Transformer.&lt;/p&gt;

&lt;h3 id=&quot;lite-transformer&quot;&gt;Lite Transformer&lt;/h3&gt;

&lt;p&gt;The &lt;a href=&quot;https://github.com/mit-han-lab/lite-transformer&quot;&gt;Lite Transformer&lt;/a&gt; is a variant of the Transformer model that makes certain trade-offs to improve performance on resource constrained devices. The authors recognised that computation in a standard Transformer is dominated by the feed-forward network (FFN). This is despite the fact that the FFN comes after Attention, and so does not mix context across tokens.&lt;/p&gt;

&lt;p&gt;A straightforward way to reduce compute demands is to shrink the embedding size directly - this reduces capacity across the entire model. Alternatively, we can reduce the attention feature dimension. While bottlenecking attention can save compute, the FFN still dominates compute, and the savings can be limited relative to the quality hit.&lt;/p&gt;

&lt;p&gt;The authors introduce a clever solution to this problem, called &lt;em&gt;Long-Short Range Attention&lt;/em&gt;.&lt;/p&gt;

&lt;h3 id=&quot;long-short-range-attention&quot;&gt;Long-Short Range Attention&lt;/h3&gt;

&lt;p&gt;Long-Short Range Attention (LSRA) splits the representation into two parallel branches - a convolutional neural network, specialised for learning short-range dependencies between tokens, and an attention block that can focus on long-range dependencies.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/machine-translation-at-the-edge-part-1/long-short-range-attention.png&quot; alt=&quot;Long-Short Range Attention&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: Figure 3 from &lt;a href=&quot;https://arxiv.org/pdf/2004.11886&quot;&gt;Lite Transformer with Long-Short Range Attention&lt;/a&gt; by Wu et al. This shows how conventional attention predominantly captures local (short-range) dependencies (b). In LSRA, the presence of the convolutional network for local context allows the attention mechanism to better capture long-range dependencies (c).&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The Lite Transformer authors use a threshold of 500M multiply-add operations to measure performance of the model in a mobile setting. By configuring various models to operate within this threshold, they showed that LSRA often outperforms pure transformer models in resource-constrained environments.&lt;/p&gt;

&lt;h3 id=&quot;other-choices&quot;&gt;Other Choices&lt;/h3&gt;

&lt;p&gt;While models such as Lite Transformer have great potential for reducing computational cost on embedded devices, there are some trade-offs we need to consider. The two we’re most concerned about are availability of pretrained models, and additional challenges in fine-tuning.&lt;/p&gt;

&lt;p&gt;For this post, I’ve shortlisted several alternatives that directly address the needs for model availability and fine-tuning:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://huggingface.co/docs/transformers/en/model_doc/t5&quot;&gt;Google’s T5&lt;/a&gt; is a versatile text-to-text transformer, capable of a variety of NLP tasks, including translation and summarization. T5 uses a generalised model that needs to be prefixed with the specific task (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;Translate English to French: I am a fish.&quot;&lt;/code&gt;). Possibly too big for our embedded use-case, especially if we only need to support one language pair.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://marian-nmt.github.io/&quot;&gt;MarianNMT&lt;/a&gt; is a pure C++ toolkit specialised for training Neural Machine Translation (NMT) models. It is often faster in inference, and capable of producing higher quality models for &lt;em&gt;specific&lt;/em&gt; language pairs. Marian has been used in production systems by Microsoft and Mozilla (via their own fork). Strong contender!&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://opennmt.net/&quot;&gt;OpenNMT&lt;/a&gt; is similar to MarianNMT in that it provides an ecosystem for NMT and neural sequence learning. The OpenNMT project also maintains &lt;a href=&quot;https://github.com/OpenNMT/CTranslate2&quot;&gt;CTranslate2&lt;/a&gt;, an efficient inference engine for transformer models. Also a strong contender!&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While CTranslate2 includes support for AArch64 (ARM 64-bit) CPUs, our ideal target is the Rockchip NPU. Both OpenNMT and MarianNMT will require a custom inference implementation and model conversion process. Due to the availability of model checkpoints for Marian, that’s where we’ll focus our attention going forward.&lt;/p&gt;

&lt;h2 id=&quot;mariannmt-vs-marianmt&quot;&gt;MarianNMT vs MarianMT&lt;/h2&gt;

&lt;p&gt;As mentioned above, &lt;a href=&quot;https://marian-nmt.github.io/&quot;&gt;MarianNMT&lt;/a&gt; is a toolkit specialised for training NMT models. Somewhat confusingly &lt;em&gt;MarianMT&lt;/em&gt; (without the N) refers to a collection of Transformer-based encoder–decoder models, &lt;a href=&quot;https://huggingface.co/docs/transformers/en/model_doc/marian&quot;&gt;published on Hugging Face&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All MarianMT variants follow the Transformer architecture, with 6 layers in both encoder and decoder. They are all natively ONNX exportable, which is good for us, because it eases the transition to RKNN format.&lt;/p&gt;

&lt;p&gt;For training, MarianMT relies on the &lt;a href=&quot;https://github.com/marian-nmt/marian&quot;&gt;MarianNMT framework&lt;/a&gt;, which is written in C++. Marian can run on the CPU, however GPU acceleration is available via CUDA/cuDNN when enabled.&lt;/p&gt;

&lt;p&gt;The MarianNMT project includes an efficient inference engine, also written in C++. This can be compiled to run on the CPU, or on an NVIDIA GPU using CUDA. We can also use an implementation based on &lt;a href=&quot;https://huggingface.co/docs/transformers/en/index&quot;&gt;Hugging Face Transformers&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;build-from-source-optional&quot;&gt;Build from Source (Optional)&lt;/h3&gt;

&lt;p&gt;Getting started with MarianNMT is relatively straightforward… assuming you have a Linux system with an NVIDIA GPU, and the necessary CUDA Toolkit / cuDNN libraries installed.&lt;/p&gt;

&lt;p&gt;To begin with, you can clone the MarianNMT source code from GitHub:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git clone https://github.com/marian-nmt/marian.git
cd marian
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then we can build it using CMake:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mkdir build &amp;amp;&amp;amp; cd build
cmake ..
make
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This builds the entire toolkit, which includes tools for training and the inference engine.&lt;/p&gt;

&lt;p&gt;The MarianNMT build depends on CUDA by default, so you won’t have much luck if you try to build this on macOS or another platform without CUDA support. To try compiling without CUDA support:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cmake .. -DUSE_SENTENCEPIECE=on -DCOMPILE_CPU=on -DCOMPILE_CUDA=off
make
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will be somewhat slower, but good enough for a simple test run.&lt;/p&gt;

&lt;p&gt;Note: This may not work on newer Macs based on Apple Silicon. Don’t worry if this means you cannot run the examples yourself. I’ve included example output below, so you can follow along!&lt;/p&gt;

&lt;h3 id=&quot;npz-format&quot;&gt;NPZ Format&lt;/h3&gt;

&lt;p&gt;Unfortunately, Hugging Face checkpoints aren’t directly usable by the Marian C++ decoder. The MarianNMT inference engine expects an .npz file…&lt;/p&gt;

&lt;p&gt;The Marian .npz format is essentially a ZIP archive containing one or more .npy (NumPy) arrays, which store model weights in a format appropriate for Marian NMT models. It is based on NumPy’s .npz format and is specifically tailored for fast loading and execution in the C++ Marian decoder.&lt;/p&gt;

&lt;p&gt;We can see how this works with a simple Python script:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;numpy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Save arrays to an npz file
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;savez&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;arrays.npz&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;array1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;array2&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Load arrays from an npz file
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;arrays.npz&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;array1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Output: [1 2 3]
&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;array2&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Output: [4 5 6]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One way we can address this is to convert a pretrained model from another format (e.g. PyTorch) to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.npz&lt;/code&gt; format. However, this can be error-prone, and it’s an unpleasant way to begin experimenting with a new architecture.&lt;/p&gt;

&lt;h3 id=&quot;pretrained-models&quot;&gt;Pretrained Models&lt;/h3&gt;

&lt;p&gt;Alternatively, we can find pretrained models that already have .npz files available. There are a range of models available, trained on various language pairs and datasets. Common datasets for Machine Translation include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;WMT - WMT (short for ‘Workshop on Machine Translation’) is a shared-task dataset created annually for Workshop on Machine Translation competitions. Includes major language pairs and is trained on news, formal text, and curated sources.&lt;/li&gt;
  &lt;li&gt;Tatoeba - A community driven dataset consisting of example sentences and translations, all provided by volunteers. This includes hundreds of languages, including many ‘low resource’ languages.&lt;/li&gt;
  &lt;li&gt;OPUS - OPUS (Open Parallel Corpus) is an umbrella project hosting hundreds of parallel corpora from many domains, scraped from the web or public sources. Great for translation, as it contains billions of sentence pairs across 400+ languages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;test-flight&quot;&gt;Test Flight&lt;/h2&gt;

&lt;p&gt;For the rest of this post, we’ll focus on an English &amp;lt;-&amp;gt; French model that was pretrained on the OPUS dataset.&lt;/p&gt;

&lt;h3 id=&quot;getting-the-model&quot;&gt;Getting the Model&lt;/h3&gt;

&lt;p&gt;The specific model we want is &lt;a href=&quot;https://github.com/Helsinki-NLP/OPUS-MT-train/tree/master/models/en-fr&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;en-fr&lt;/code&gt;&lt;/a&gt; from the &lt;a href=&quot;https://github.com/Helsinki-NLP/OPUS-MT-train&quot;&gt;OPUS-MT-train&lt;/a&gt; repo on GitHub. The model is a few years old now, having been trained in 2020. However, it’s in NPZ format, ensuring that we can use it directly with MarianNMT.&lt;/p&gt;

&lt;p&gt;The model’s README file provides a link to a download mirror. Let’s download and extract the model:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;wget https://object.pouta.csc.fi/OPUS-MT-models/en-fr/opus-2020-02-26.zip
unzip -d opus-2020-02-26 opus-2020-02-26.zip
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note: The instructions below assume you’ve extracted the model to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;marian/build/opus-2020-02-26&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The zip file will be about ~265MB, and the extracted model will take about 288MB on disk. The file list when extracting may be a bit intimidating at first:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Archive:  opus-2020-02-26.zip
  inflating: opus-2020-02-26/README.md
  inflating: opus-2020-02-26/LICENSE
  inflating: opus-2020-02-26/opus.spm32k-spm32k.transformer-align.model1.npz.best-perplexity.npz
  inflating: opus-2020-02-26/opus.spm32k-spm32k.vocab.yml
  inflating: opus-2020-02-26/opus.spm32k-spm32k.transformer-align.valid1.log
  inflating: opus-2020-02-26/opus.spm32k-spm32k.transformer-align.train1.log
  inflating: opus-2020-02-26/source.spm
 extracting: opus-2020-02-26/source.tcmodel
  inflating: opus-2020-02-26/target.spm
  inflating: opus-2020-02-26/decoder.yml
  inflating: opus-2020-02-26/preprocess.sh
  inflating: opus-2020-02-26/postprocess.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There are several files that are worth pointing out. The first is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opus.spm32k-spm32k.transformer-align.model1.npz.best-perplexity.npz&lt;/code&gt;, which contains Marian model weights in NPZ format. This is the core of the model. The string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spm32k&lt;/code&gt; tells us that the specific tokenizer is SentencePiece (spm) with a vocab size of 32,000.&lt;/p&gt;

&lt;p&gt;The next two files of interest are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source.spm&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target.spm&lt;/code&gt;, which are SentencePiece models. These models contain the information needed to segment an input sentence into tokens (i.e subword parts), and to concatenate generated tokens into the final output. This leads us to a more in-depth discussion on tokenization…&lt;/p&gt;

&lt;h3 id=&quot;tokenization&quot;&gt;Tokenization&lt;/h3&gt;

&lt;p&gt;When translating a piece of text, that input must first be segmented into units of text called tokens. Tokens may be entire words, individual characters, or groups of characters (i.e. subwords):&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/machine-translation-at-the-edge-part-1/kinds-of-tokenization.png&quot; alt=&quot;Kinds of Tokenization&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Token granularity is an implementation detail that must be included in a model definition. MarianNMT often uses SentencePiece to tokenize text into subwords. SentencePiece itself supports the &lt;a href=&quot;https://huggingface.co/learn/llm-course/en/chapter6/7&quot;&gt;Unigram&lt;/a&gt; and &lt;a href=&quot;https://huggingface.co/learn/llm-course/en/chapter6/5&quot;&gt;Byte-Pair Encoding&lt;/a&gt; (BPE) algorithms. Which one is used is model-dependent.&lt;/p&gt;

&lt;p&gt;To make this more concrete, we can use Marian’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spm_encode&lt;/code&gt; to tokenize an input sentence (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;I am a spiky fish&quot;&lt;/code&gt;) using the source tokenizer:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ echo &quot;I am a spiky fish&quot; | ./spm_encode --model opus-2020-02-26/source.spm
I ▁am ▁a ▁sp ik y ▁fish
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can see that the output is a series of tokens (or subword parts), but our model requires integer token IDs. The last file we’re interested in is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opus.spm32k-spm32k.vocab.yml&lt;/code&gt; - this provides a ‘vocabulary’, mapping tokens to integers (and vice-versa).&lt;/p&gt;

&lt;h3 id=&quot;vocab-file&quot;&gt;Vocab File&lt;/h3&gt;

&lt;p&gt;The vocab YAML file is human readable, so we can take a peek inside… This is the first 20 lines of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opus.spm32k-spm32k.vocab.yml&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ head -n 20 opus-2020-02-26/opus.spm32k-spm32k.vocab.yml
&amp;lt;/s&amp;gt;: 0
&amp;lt;unk&amp;gt;: 1
&quot;,&quot;: 2
.: 3
▁the: 4
▁de: 5
&quot;&apos;&quot;: 6
▁of: 7
▁la: 8
s: 9
▁and: 10
▁et: 11
▁to: 12
▁des: 13
▁l: 14
▁a: 15
▁les: 16
▁à: 17
▁in: 18
▁le: 19
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The list begins with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;/s&amp;gt;&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;unk&amp;gt;&lt;/code&gt;, which represent end-of-sequence and unknown strings, respectively. Unknown strings occur when the input contains character sequences that do not have a valid mapping.&lt;/p&gt;

&lt;p&gt;Next, we see some common symbols &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;,&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.&lt;/code&gt;, followed by strings that look like parts of words. These are often prefixed with a whitespace marker &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;▁&lt;/code&gt;, which bears a striking resemblence to an underscore… The whitespace marker means that these are prefixes - the first part of a word that occurs after a space. This distinction between subwords that follow a space and those that don’t has been shown to improve model performance.&lt;/p&gt;

&lt;h3 id=&quot;full-pipeline&quot;&gt;Full Pipeline&lt;/h3&gt;

&lt;p&gt;With all that covered, we can invoke the model using a Unix pipeline consisting of three executables: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spm_encode&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;marian-decoder&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spm_decode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can invoke the decoder using this rather complicated command:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;echo &quot;I am a fish&quot; \
  | ./spm_encode \
      --model opus-2020-02-26/source.spm \
  | ./marian-decoder \
      --models \
        opus-2020-02-26/opus.spm32k-spm32k.transformer-align.model1.npz.best-perplexity.npz \
      --vocabs \
        opus-2020-02-26/opus.spm32k-spm32k.vocab.yml \
        opus-2020-02-26/opus.spm32k-spm32k.vocab.yml \
      --devices 0 \
      --beam-size 6 \
      --normalize 1.0 \
  | ./spm_decode \
      --model opus-2020-02-26/target.spm
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Starting at the top, we use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;echo&lt;/code&gt; to output the string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;I am a fish&quot;&lt;/code&gt;. This is piped to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spm_encode&lt;/code&gt; so that SentencePiece can tokenize the input. Tokens are then piped to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;marian-decoder&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note that when we invoke &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;marian-decoder&lt;/code&gt;, the same vocab file is provided twice - this is not a typo. Marian models support &lt;em&gt;asymmetric&lt;/em&gt; translation - i.e. using different vocabularies for the source language (encoder) and target languages (decoder). Therefore, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;marian-decoder&lt;/code&gt; command expects paths for both source and target vocabulary files. As we saw above, this file contains a mix of English and French tokens, so we can use the same file for both.&lt;/p&gt;

&lt;p&gt;Finally, the output of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;marian-decoder&lt;/code&gt; is piped to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spm_decode&lt;/code&gt; to be converted into human-readable text!&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[2026-01-04 19:53:04] [config] Loaded model has been created with Marian v1.8.2 2111c28 2019-10-16 08:36:48 -0700
...
[2026-01-04 19:53:05] Loaded model config
[2026-01-04 19:53:05] Loading scorer of type transformer as feature F0
[2026-01-04 19:53:05] [memory] Reserving 284 MB, device gpu0
[2026-01-04 19:53:05] [gpu] 16-bit TensorCores enabled for float32 matrix operations
[2026-01-04 19:53:05] Best translation 0 : ▁Je ▁suis ▁un ▁poisson
Je suis un poisson
[2026-01-04 19:53:05] Total time: 0.05016s wall
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If we comb through the output, we find the string ‘Je suis un poisson’, which is a valid French translation of ‘I am a fish’!&lt;/p&gt;

&lt;h3 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h3&gt;

&lt;p&gt;That completes our introduction to machine translation and the MarianNMT toolkit. We’ve covered a lot of ground! However, everything we’ve done so far has been on PC, rather than our target device.&lt;/p&gt;

&lt;p&gt;In part two, we turn our attention to the Rockchip NPU. Our goal will be to perform MarianMT inference on the Rockchip NPU. We will download a pretrained MarianMT model from Hugging Face, convert it to RKNN format, then run it directly on Rockchip NPU.&lt;/p&gt;

&lt;p&gt;To make all of this work, we’ll need to consider quantization, input padding and layer compatibility. Thankfully, the RKNN Python API makes this much easier than it sounds. Our result will be a Marian implementation that runs directly on the Rockchip NPU!&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;p&gt;Historical:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://aclanthology.org/www.mt-archive.info/50/MIT-1952-TOC.htm&quot;&gt;MIT Conference on Mechanical Translation&lt;/a&gt; - Mostly unpublished papers from the early days of Machine Translation.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www2.statmt.org/moses/&quot;&gt;Moses SMT&lt;/a&gt; - This is worth exploring, if only to appreciate the extensive research that went into constructing Machine Translation systems prior to the rise of Transformers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NMT Systems:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://marian-nmt.github.io/&quot;&gt;MarianNMT Home Page&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://opennmt.net/&quot;&gt;OpenNMT Home Page&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tokenization:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://huggingface.co/learn/llm-course/en/chapter6/5&quot;&gt;Byte-Pair Encoding - Hugging Face&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://huggingface.co/learn/llm-course/en/chapter6/7&quot;&gt;Unigram - Hugging Face&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://colabdoge.medium.com/understanding-sentencepiece-under-standing-sentence-piece-ac8da59f6b08&quot;&gt;Understanding SentencePiece&lt;/a&gt; - A nice introduction to SentencePiece.&lt;/li&gt;
&lt;/ul&gt;
</description>
					<pubDate>Mon, 05 Jan 2026 00:00:00 +0000</pubDate>
					<link>https://tristanpenman.com/blog/posts/2026/01/05/machine-translation-at-the-edge-part-1/</link>
					<guid isPermaLink="true">https://tristanpenman.com/blog/posts/2026/01/05/machine-translation-at-the-edge-part-1/</guid>
				</item>
			
		
			
				<item>
					<title>Tiny Language Models </title>
					
					<description>&lt;p&gt;This post continues on the theme of Edge AI. We &lt;a href=&quot;https://tristanpenman.com/blog/posts/2025/07/20/edge-ai-using-the-rockchip-npu/&quot;&gt;previously covered&lt;/a&gt; the basics of Edge AI and the practicalities of running YOLO, an Object Detection algorithm, on the Rockchip NPU. In this post, we’ll look at the history and scaling challenges of Large Language Models and what it means to train and deploy a &lt;em&gt;tiny&lt;/em&gt; language model.&lt;/p&gt;

&lt;p&gt;Although there is an example for the &lt;a href=&quot;https://www.khadas.com/edge2&quot;&gt;Khadas Edge2&lt;/a&gt; at the end, the content here is mostly foundational in nature. In a later post, we will look at practical examples of running Machine Translation algorithms on the Rockchip NPU.&lt;/p&gt;

&lt;h3 id=&quot;contents&quot;&gt;Contents&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#a-brief-history-of-language-models&quot;&gt;A Brief History of Language Models…&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#n-gram-models&quot;&gt;N-gram Models&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#neural-language-models&quot;&gt;Neural Language Models&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#recurrent-neural-networks&quot;&gt;Recurrent Neural Networks&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#transformers&quot;&gt;Transformers&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#large-language-models&quot;&gt;Large Language Models&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#hardware-constraints&quot;&gt;Hardware Constraints&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#model-architecture-support&quot;&gt;Model Architecture Support&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#llm-families&quot;&gt;LLM Families&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#transformer-architecture&quot;&gt;Transformer Architecture&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#self-attention&quot;&gt;Self-Attention&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#scaled-dot-product-attention&quot;&gt;Scaled Dot-Product Attention&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#cross-attention&quot;&gt;Cross-Attention&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#encoders-and-decoders&quot;&gt;Encoders and Decoders&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#model-structure&quot;&gt;Model Structure&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#recommended-resources&quot;&gt;Recommended Resources&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#tiny-language-models&quot;&gt;Tiny Language Models&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#model-distillation&quot;&gt;Model Distillation&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#tinyllama&quot;&gt;TinyLlama&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#qwen&quot;&gt;Qwen&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#tinybert-and-distilbert&quot;&gt;TinyBERT and DistilBERT&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#tokenization&quot;&gt;Tokenization&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#kinds-of-tokenization&quot;&gt;Kinds of Tokenization&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#sub-word-tokenization&quot;&gt;Sub-Word Tokenization&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#de-tokenization&quot;&gt;De-Tokenization&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#greedy-decoding&quot;&gt;Greedy Decoding&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#beam-search&quot;&gt;Beam Search&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#rkllm-toolkit&quot;&gt;RKLLM Toolkit&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#getting-started-with-rkllm&quot;&gt;Getting Started with RKLLM&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#deepseek-r1-distill-qwen&quot;&gt;DeepSeek-R1-Distill-Qwen&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#example-prompt&quot;&gt;Example Prompt&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#next-steps&quot;&gt;Next Steps…&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#machine-translation-at-the-edge&quot;&gt;Machine Translation at the Edge&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#tokenization-deep-dive&quot;&gt;Tokenization Deep Dive&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#references&quot;&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;a-brief-history-of-language-models&quot;&gt;A Brief History of Language Models…&lt;/h2&gt;

&lt;p&gt;The history of language models (or more generally, Natural Language Processing) features some clearly visible paradigm shifts. In the early days of computing, computer scientists and programmers were quick to explore the AI landscape. As early as the 1960s, chatbots such as of Joseph Weizenbaum’s &lt;a href=&quot;https://en.wikipedia.org/wiki/ELIZA&quot;&gt;ELIZA&lt;/a&gt; led people to believe that the era of machine intelligence was upon us.&lt;/p&gt;

&lt;p&gt;Fast forward to the 1980s and 90s, and by this time N-gram models had taken hold. These probabilistic models aimed to predict words and symbols that would occur in a text, using the surrounding context to condition the probability. Although the results were compelling, they had properties that made them very difficult to scale.&lt;/p&gt;

&lt;p&gt;In the early 2000s, neural networks were successfully applied to language modeling. The immense representational power of neural networks enabled the development of word embeddings, like those seen in &lt;a href=&quot;https://en.wikipedia.org/wiki/Word2vec&quot;&gt;Word2Vec&lt;/a&gt;. Although vanilla neural networks quickly reached their limits, research in the 2010s gave us Recurrent Neural Networks and LSTMs, which allowed neurons to preserve state from one word/symbol to the next.&lt;/p&gt;

&lt;p&gt;Finally, the invention of Transformers in 2017 ushered in the era of &lt;em&gt;Large Language Models&lt;/em&gt;. You can see from the timeline below that we’re currently in a period of rapid growth, marked not just by leaps in computational capacity, but also paradigm shifts:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/timeline-of-language-models.png&quot; alt=&quot;Timeline of Language Models&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We’ll dive into each of these in a bit more detail.&lt;/p&gt;

&lt;h3 id=&quot;n-gram-models&quot;&gt;N-gram Models&lt;/h3&gt;

&lt;p&gt;N-gram models are purely probabilistic language models, trained on a body of text (commonly known in NLP as a “corpus”) to capture the frequency and context in which different symbols occur. Symbols can be individual characters, words, or even sub-words (e.g. prefixes and suffixes).&lt;/p&gt;

&lt;p&gt;N-gram models are characterised by the length &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N&lt;/code&gt; of the input sequence used to condition the probability of a symbol that follows in the sequence. Assuming that entire words are used as symbols:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/n-gram-models.png&quot; alt=&quot;N-Gram Models&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Using Bayes’ rule (or &lt;a href=&quot;https://en.wikipedia.org/wiki/Bayes%27_theorem&quot;&gt;Bayes’ theorem&lt;/a&gt;), we can calculate the probability of a given input sequence as a joint probability:&lt;/p&gt;

\[\begin{aligned}
&amp;amp;P(\text{The}\text{ }{sky}\text{ }{is}\text{ }{blue})
\\
= &amp;amp;P({The}, {sky}, {is}, {blue})
\\
= &amp;amp;P({The}) * P({sky}\text{ }|\text{ }{The}) * P({is}\text{ }|\text{ }{The}, {sky}) * P({blue}\text{ }|\text{ }{The}, {sky}, {is})
\end{aligned}\]

&lt;p&gt;N-gram models suffer from two critical limitations. First—they have limited expressive capabilities. Specifically, the probability of a particular symbol occurring may be dependent on context that falls outside the conditioning sequence. We can attempt to address this by increasing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N&lt;/code&gt; to capture more context.&lt;/p&gt;

&lt;p&gt;This leads to the second issue - as the value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N&lt;/code&gt; increases, the data and storage requirements also grow exponentially. And if the dataset is not sufficiently large and diverse, the model risks overfitting.&lt;/p&gt;

&lt;h3 id=&quot;neural-language-models&quot;&gt;Neural Language Models&lt;/h3&gt;

&lt;p&gt;The introduction of &lt;a href=&quot;https://www.jmlr.org/papers/volume3/bengio03a/bengio03a.pdf&quot;&gt;Neural Language Models in 2003&lt;/a&gt; also gave us the concept of &lt;em&gt;Word Embeddings&lt;/em&gt;. A Word Embedding is a vector representation of a word, where each component in the vector corresponds to some concept captured by the model.&lt;/p&gt;

&lt;p&gt;Let’s make this more concrete. Say you have a vocabulary of five words, and each word is represented using its &lt;a href=&quot;https://en.wikipedia.org/wiki/One-hot&quot;&gt;one-hot encoding&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Cat    -&amp;gt; [1, 0, 0, 0, 0]
Kitten -&amp;gt; [0, 1, 0, 0, 0]
Dog    -&amp;gt; [0, 0, 1, 0, 0]
Puppy  -&amp;gt; [0, 0, 0, 1, 0]
Bird   -&amp;gt; [0, 0, 0, 0, 1]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If we used these vectors as inputs for a neural network, the input layer will require five neurons: one for each component of the input vector. Unfortunately, as the vocab grows in size, say to 40,000 words, the input layer of our neural network will also grow to be 40,000 neurons wide.&lt;/p&gt;

&lt;p&gt;Besides scaling issues, one-hot encoding comes with two other disadvantages:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Sparsity - Most values in each vector are zeros. This is wasteful of storage, and means that the first layer of the network has relatively little information to work with.&lt;/li&gt;
  &lt;li&gt;No Relationships Between Words - The disjoint nature of one-hot vectors means that we cannot derive any meaningful relationships between words.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To highlight the second point, consider the first two examples: &lt;em&gt;Cat&lt;/em&gt; and &lt;em&gt;Kitten&lt;/em&gt;. A Kitten is a kind of Cat, so it doesn’t make sense for those to be completely disjoint in their representations. Word Embeddings address this by learning a dense representation (an embedding) for each word in the dataset. The embedding reduces the dimensionality of the input data, allowing the rest of the model to operate on vectors with a lower dimensionality.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/neural-language-model.png&quot; alt=&quot;Neural Language Model&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: Figure 1 from &lt;a href=&quot;https://www.jmlr.org/papers/volume3/bengio03a/bengio03a.pdf&quot;&gt;A Neural Probabilistic Language Model&lt;/a&gt; by Bengio et al (2003).&lt;br /&gt;The major contribution of Bengio et al’s paper was train a neural language model end-to-end.&lt;/small&gt;&lt;/p&gt;

&lt;h4 id=&quot;word2vec&quot;&gt;Word2Vec&lt;/h4&gt;

&lt;p&gt;A critical issue with Neural Language Models is that training at scale was expensive and suffered from tight coupling between network architecture and word representations. In 2013, &lt;a href=&quot;https://en.wikipedia.org/wiki/Word2vec&quot;&gt;Word2Vec&lt;/a&gt; showed that it was possible to learn meaningful relationships between words by training a shallow network on a large corpus. The resulting word embeddings could then be re-used across different architectures.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/word2vec-embeddings.png&quot; alt=&quot;Word2Vec Embeddings&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: &lt;a href=&quot;https://jalammar.github.io/illustrated-word2vec/&quot;&gt;The Illustrated Word2Vec&lt;/a&gt; by Jay Alammar.&lt;br /&gt;He goes into great detail about how Word2Vec can be used in a variety of applications.&lt;/small&gt;&lt;/p&gt;

&lt;h3 id=&quot;recurrent-neural-networks&quot;&gt;Recurrent Neural Networks&lt;/h3&gt;

&lt;p&gt;Recurrent Neural Networks (RNNs) made it possible to preserve some state from one token to the next. This meant that generated tokens would take more context into account - for example, choosing the generated token based on the meaning implied by the most recent words in the input sequence.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/recurrent-neural-network.png&quot; alt=&quot;Recurrent Neural Network&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: &lt;a href=&quot;https://www.analyticsvidhya.com/blog/2022/03/a-brief-overview-of-recurrent-neural-networks-rnn/&quot;&gt;A Brief Overview of Recurrent Neural Networks&lt;/a&gt; from Analytics Vidhya.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;A key problem with RNNs is &lt;a href=&quot;https://en.wikipedia.org/wiki/Vanishing_gradient_problem&quot;&gt;vanishing gradients&lt;/a&gt;. Long Short-Term Memory (LSTM) networks build upon RNNs by introducing a new internal structure…&lt;/p&gt;

&lt;h4 id=&quot;long-short-term-memory-networks&quot;&gt;Long-Short Term Memory Networks&lt;/h4&gt;

&lt;p&gt;Although the idea for &lt;a href=&quot;https://en.wikipedia.org/wiki/Long_short-term_memory&quot;&gt;Long Short-Term Memory&lt;/a&gt; (LSTM) networks was first described in 1997 by Hochreiter et al, it wasn’t until the 2010s that this found wider adoption in Language Models. LSTMs attempt to solve the vanishing gradient problem by ensuring that errors flow back through the network, while introducing gating mechanisms that allow the error (or gradient) to be truncated when it is beneficial.&lt;/p&gt;

&lt;p&gt;However, this wasn’t quite enough to get us to where we are now… RNNs and LSTMs are inherently serial, which causes severe bottlenecks. Vanishing gradients are a problem, even in LSTM networks. This makes it increasingly difficult for models to learn connections between tokens over longer time spans.&lt;/p&gt;

&lt;h3 id=&quot;transformers&quot;&gt;Transformers&lt;/h3&gt;

&lt;p&gt;The NLP landscape changed dramatically in 2017, when the seminal paper &lt;a href=&quot;https://arxiv.org/abs/1706.03762&quot;&gt;Attention Is All You Need&lt;/a&gt; introduced the Transformer architecture. The Transformer architecture replaces recurrence with a mechanism called “Attention.” This made it possible for language models to capture long-range dependencies.&lt;/p&gt;

&lt;p&gt;You’re almost certainly aware of Transformers (although you may not recognise them by name). They are the “T” in GPT (short for Generative Pre-trained Transformers) and have led to many state-of-the-art results in Natural Language Processing and Machine Learning tasks.&lt;/p&gt;

&lt;h2 id=&quot;large-language-models&quot;&gt;Large Language Models&lt;/h2&gt;

&lt;p&gt;Current state of the art language models contain billions of parameters, and must be trained on massive datasets. As a reference point, we’ll use &lt;a href=&quot;https://arxiv.org/abs/2005.14165&quot;&gt;OpenAI’s GPT-3&lt;/a&gt;, since there are some reasonably well grounded numbers available online.&lt;/p&gt;

&lt;p&gt;Just the pretraining phase of GPT-3 relied on approximately 570GB of text content, pulled from sources such as &lt;a href=&quot;https://commoncrawl.org/&quot;&gt;Common Crawl&lt;/a&gt;, &lt;a href=&quot;https://openwebtext2.readthedocs.io/en/latest/&quot;&gt;OpenWebText2&lt;/a&gt; and Wikipedia. Prior to training, this content is converted to tokens through a process called tokenization. The resulting training set contains approximately 300 billion tokens.&lt;/p&gt;

&lt;p&gt;The largest GPT-3 model uses 175 billion parameters. This translates to roughly 350GB if you store the weights as 16-bit floating points (FP16). This poses many architectural challenges, even when working with data-center class GPUs. It is currently impossible to load so much data onto a single GPU. Therefore, the weights for Large Language Models must be distributed across multi-GPU clusters, with the clusters themselves connected via high-bandwidth interconnects.&lt;/p&gt;

&lt;h3 id=&quot;hardware-constraints&quot;&gt;Hardware Constraints&lt;/h3&gt;

&lt;p&gt;So what do we do when we want to run a Large Language Model on an embedded device?&lt;/p&gt;

&lt;p&gt;We face a number of challenges when targeting an embedded environment. First is limited memory (RAM/VRAM). LLMs use a lot of RAM for model weights and context windows. Besides limited RAM, the memory architecture may not be as fast as desktop / server RAM.&lt;/p&gt;

&lt;p&gt;Next is limited computing power. While many embedded devices now feature NPUs (Neural Processing Units) these are still very limited compared to desktop and server-grade GPUs.&lt;/p&gt;

&lt;h3 id=&quot;model-architecture-support&quot;&gt;Model Architecture Support&lt;/h3&gt;

&lt;p&gt;Finally, model architecture support may be limited. Embedded NPUs are simpler in nature than desktop and cloud GPUs. They often lack some of the hardware-accelerated operations required to implement a model using the Transformer architecture. Those operations need to be implemented using slower CPU-based emulation.&lt;/p&gt;

&lt;p&gt;Attention kernels and rotary embeddings are two examples that are often not supported.&lt;/p&gt;

&lt;h2 id=&quot;transformer-architecture&quot;&gt;Transformer Architecture&lt;/h2&gt;

&lt;p&gt;Understanding the Transformer Architecture will get us a little closer to the question of why language models are so large in the first place. We’ll briefly touch upon the concepts of Self-Attention, Cross-Attention, and the model structures typically seen in LLMs.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/transformer-architecture.png&quot; alt=&quot;Transformer Architecture&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: Figure 1 from &lt;a href=&quot;https://arxiv.org/abs/1706.03762&quot;&gt;Attention Is All You Need&lt;/a&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;h3 id=&quot;self-attention&quot;&gt;Self-Attention&lt;/h3&gt;

&lt;p&gt;Self-Attention is a mechanism that allows each token in a sequence to decide which other tokens are relevant to it and to aggregate that information via a weighted sum. The result is a context-dependent token representation, which can “look” backwards as well as forwards in the input sequence.&lt;/p&gt;

&lt;p&gt;This is in contrast to RNNs and LSTMs—these models could pass context forward from one state to the next; however, they were restricted to encoding that information in hidden state that was of a fixed size. They also suffered from &lt;a href=&quot;https://en.wikipedia.org/wiki/Vanishing_gradient_problem&quot;&gt;vanishing gradients&lt;/a&gt;, and were unidirectional in nature.&lt;/p&gt;

&lt;h3 id=&quot;scaled-dot-product-attention&quot;&gt;Scaled Dot-Product Attention&lt;/h3&gt;

&lt;p&gt;We hand-waved an important detail above: the &lt;strong&gt;weighted sum&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each token’s initial representation is projected into query, key, and value vectors. The query vector is used to calculate a similarity score with the keys for every other token in the sequence. We then apply a softmax to the similarity scores to find attention weights. Those weights are used to calculate a weighted sum of the corresponding value vectors. The final result is a context-dependent representation of a token. This whole process is called &lt;em&gt;Scaled Dot-Product Attention&lt;/em&gt;.&lt;/p&gt;

\[\text{Attention}(Q, K, V) = \text{softmax}\left( \frac{Q K^\top}{\sqrt{d_k}} \right) V\]

&lt;p&gt;That paragraph was a lot to digest! If this is new to you, I highly recommend the &lt;a href=&quot;https://www.3blue1brown.com/topics/neural-networks&quot;&gt;Neural Networks course&lt;/a&gt; from &lt;a href=&quot;https://www.3blue1brown.com/&quot;&gt;3blue1brown&lt;/a&gt;. Specifically, the content on &lt;a href=&quot;https://www.3blue1brown.com/lessons/attention&quot;&gt;Attention in Transformers&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;cross-attention&quot;&gt;Cross-Attention&lt;/h3&gt;

&lt;p&gt;Scaled Dot-Product Attention is also used in Cross-Attention. Where Self-Attention asks “How do tokens in this sequence relate to one other?”, Cross-Attention asks “How do tokens in this sequence relate to tokens in another sequence?”&lt;/p&gt;

&lt;p&gt;Both Self-Attention and Cross-Attention use the Scaled Dot-Product Attention formula that we saw above. The only difference is how we construct Q, K, and V.&lt;/p&gt;

&lt;p&gt;In Self-Attention, Q, K, and V are all derived from the same sequence of hidden states. You can think of this as trying to predict the next token in a single sequence.&lt;/p&gt;

&lt;p&gt;In Cross-Attention, Q comes from one sequence, while K and V come from another, distinct sequence. So you can think of this as trying to predict the next token in an output sequence, given the tokens seen in a &lt;em&gt;different&lt;/em&gt; input sequence.&lt;/p&gt;

&lt;h3 id=&quot;encoders-and-decoders&quot;&gt;Encoders and Decoders&lt;/h3&gt;

&lt;p&gt;Cross-Attention was originally developed to support Machine Translation and Text Summarisation tasks. In these architectures, one half of the model is known as the Decoder, and the other half is the Encoder. Cross-Attention allows the decoder to &lt;em&gt;attend to&lt;/em&gt; outputs of the encoder, while generating a distinct sequence of tokens. This allows the output to reflect the meaning of the original input, but transformed in some meaningful way - e.g. translated to a different output language.&lt;/p&gt;

&lt;p&gt;The basic structure of an Encoder-Decoder Architecture is shown below:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/encoder-decoder-architecture.png&quot; alt=&quot;Encoder / Decoder Architecture&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It’s also possible to have encoder-only or decoder-only models. Google’s &lt;a href=&quot;https://en.wikipedia.org/wiki/BERT_(language_model)&quot;&gt;BERT&lt;/a&gt; is an example of an encoder-only model, while GPT is an example of a decoder-only model. In both cases, only Self-Attention is used.&lt;/p&gt;

&lt;p&gt;The key difference between GPT (decoder-only) and BERT (encoder-only) lies in how they are trained. BERT is trained using a &lt;em&gt;bidirectional&lt;/em&gt; approach, whereby tokens are permitted to attend to any other token in the input text. GPT is &lt;em&gt;unidirectional&lt;/em&gt;, and uses masking techniques to ensure that the model cannot “look into the future”.&lt;/p&gt;

&lt;h3 id=&quot;model-structure&quot;&gt;Model Structure&lt;/h3&gt;

&lt;p&gt;Transformer models tend to follow a similar architecture.&lt;/p&gt;

&lt;p&gt;The first part of the model is the tokenizer—this is the part of the network that splits the input into individual tokens. We explore this in more detail in the &lt;a href=&quot;#tokenization&quot;&gt;Tokenization&lt;/a&gt; section below.&lt;/p&gt;

&lt;p&gt;Following the tokenizer is the backbone. This is the core of the neural network, implementing the attention mechanism used by the model to generate an output sequence. However, the output of this layer cannot be used as-is. It’s a highly specialised representation of the model’s hidden state.&lt;/p&gt;

&lt;p&gt;The last part of the model is the head (sometimes called the “LM head”). This is a standard linear layer that projects each hidden state to a vector of size &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vocab_size&lt;/code&gt;. This vector is used to choose tokens (essentially the reverse of tokenisation), based on the confidence assigned to each possible token.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/transformer-llm.png&quot; alt=&quot;Transformer LLM&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;recommended-resources&quot;&gt;Recommended Resources&lt;/h3&gt;

&lt;p&gt;We’ve skipped over many other details, such as Positional Encoding, which are important to understand when diving into LLMs. To learn more, I recommend picking up a copy of &lt;a href=&quot;https://www.oreilly.com/library/view/hands-on-large-language/9781098150952/&quot;&gt;Hands-On Large Language Models&lt;/a&gt; by Jay Alammar and Maarten Grootendorst.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.oreilly.com/library/view/hands-on-large-language/9781098150952/&quot;&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/hands-on-llms-book.jpg&quot; alt=&quot;Hands-on LLMs Book&quot; class=&quot;shadow&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Hands-on Large Language Models covers a wide range of topics. Beyond just LLM fundamentals, it explores different ways to chain models together to build sophisticated LLM-powered applications.&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;tiny-language-models&quot;&gt;Tiny Language Models&lt;/h2&gt;

&lt;p&gt;With Large Language Models out of the way, we can shift our attention to &lt;em&gt;Tiny&lt;/em&gt; Language Models.&lt;/p&gt;

&lt;p&gt;Tiny Language Models are typically trained using fewer parameters or other simplifications such that they will fit within the constraints of an embedded device. We’ll look at just a few here, but be aware there are many others!&lt;/p&gt;

&lt;h3 id=&quot;model-distillation&quot;&gt;Model Distillation&lt;/h3&gt;

&lt;p&gt;A key ingredient in creating Tiny Language Models is model distillation. This is the process by which a compact “student” model can be trained using the outputs of a much larger “teacher” model. This allows the student model to capture concepts learned by the teacher, without direct exposure to the training set.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/model-distillation.png&quot; alt=&quot;Model Distillation&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;tinyllama&quot;&gt;TinyLlama&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://ollama.com/library/tinyllama&quot;&gt;TinyLlama&lt;/a&gt; is a compact language model that copies the architecture and tokenizer of &lt;a href=&quot;https://www.llama.com/llama2/&quot;&gt;Llama 2&lt;/a&gt; (Meta’s open-source language model) but shrinks the parameter count to make it compatible with modest GPUs, CPUs, and edge devices. With just 1.1B parameters, TinyLLama is approximately 150x smaller than GPT-3.&lt;/p&gt;

&lt;p&gt;Because TinyLlama is architecturally compatible with Llama 2, it can be used with many existing Llama-based tools and projects, and there are multiple downstream variants mirroring those of the base model. For example, the &lt;a href=&quot;https://huggingface.co/TinyLlama/TinyLlama-1.1B-Chat-v0.6&quot;&gt;TinyLlama-1.1B-Chat&lt;/a&gt; variant is fine-tuned on the &lt;a href=&quot;https://huggingface.co/datasets/stingning/ultrachat&quot;&gt;UltraChat&lt;/a&gt; dataset.&lt;/p&gt;

&lt;h3 id=&quot;qwen&quot;&gt;Qwen&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://qwen.ai/&quot;&gt;Qwen&lt;/a&gt; (also known as Tongyi Qianwen) is a family of large language models developed by Alibaba Cloud. Qwen is released in a variety of model sizes (all published on &lt;a href=&quot;https://huggingface.co/Qwen&quot;&gt;Hugging Face&lt;/a&gt;), so it’s likely that you’ll find one that fits within your constraints.&lt;/p&gt;

&lt;p&gt;At time of writing, the latest iteration is Qwen 3. However, earlier iterations are often preferred for embedded AI, due to their smaller size and simpler architectures. Another advantage of Qwen is that it is released with open weights and a permissive license, and it sits in a sweet spot for “tiny” and “mid-size” local models.&lt;/p&gt;

&lt;p&gt;The underlying architecture also works well with quantization schemes. Quantization reduces the precision of model weights and activations, making it easier to fit within the constraints of an embedded device. We’ll look at a Qwen variant in the example at the end of this post.&lt;/p&gt;

&lt;h3 id=&quot;tinybert-and-distilbert&quot;&gt;TinyBERT and DistilBERT&lt;/h3&gt;

&lt;p&gt;TinyBERT and DistilBERT are both distilled versions of Google’s 2018 language model, &lt;a href=&quot;https://github.com/google-research/bert&quot;&gt;BERT&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;DistilBERT (&lt;a href=&quot;https://arxiv.org/pdf/1910.01108&quot;&gt;released by Hugging Face in 2019&lt;/a&gt;) is basically a smaller BERT (66M parameters) that you can fine-tune like normal, with no custom distillation pipeline required. What this means is that you can fine-tune the smaller model directly. In practice, it may perform slightly worse than full BERT, but it can be good enough for many applications while offering better speed and reduced memory usage.&lt;/p&gt;

&lt;p&gt;TinyBERT, from Huawei’s &lt;a href=&quot;http://dev3.noahlab.com.hk/&quot;&gt;Noah’s Ark Lab&lt;/a&gt;, uses a two-stage distillation approach (first pass is general, then task-specific). It comes in 4-layer and 6-layer versions. The 4-layer variant is even smaller than DistilBERT (just 14.5M parameters) and considerably faster while offering similar accuracy. The 6-layer variants typically use 67M parameters, depending on configuration.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/bert-variations.png&quot; alt=&quot;BERT Variations&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;DistilBERT is often preferable unless you rely on the very small footprint offered by the TinyBERT 4-Layer variation. TinyBERT’s more aggressive distillation process may come at the cost of some accuracy. However, this may be a worthwhile trade-off when building for Edge AI devices.&lt;/p&gt;

&lt;h2 id=&quot;tokenization&quot;&gt;Tokenization&lt;/h2&gt;

&lt;p&gt;Tokenization is the very first step in processing text input - it is the process by which raw text input is broken down into smaller units that can be handled by a language model. This critical first step reduces the length of the sequence that a model has to process, while retaining important context.&lt;/p&gt;

&lt;p&gt;Tokenization is a deceptively broad topic, and although it’s a critical component in modern SOTA language models, we won’t cover it in great detail here. It really deserves its own post!&lt;/p&gt;

&lt;h3 id=&quot;kinds-of-tokenization&quot;&gt;Kinds of Tokenization&lt;/h3&gt;

&lt;p&gt;There is, unfortunately, no one-size-fits-all approach to tokenization. First of all, we have determine whether we want tokens that represent characters, words, or sub-words:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/kinds-of-tokenization.png&quot; alt=&quot;Kinds of Tokenization&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Some models even operate on entire sentences - in this case we would break a text into individual sentences, which are processed in their entirety. This is generally NOT relevant these days.&lt;/p&gt;

&lt;p&gt;While sub-word tokenization is currently the most common approach used in SOTA models, character-based (or tokenization-free) models have begun to gain traction. We’ll skip of tokenization-free models here, and focus on &lt;em&gt;Sub-Word Tokenization&lt;/em&gt;.&lt;/p&gt;

&lt;h3 id=&quot;sub-word-tokenization&quot;&gt;Sub-Word Tokenization&lt;/h3&gt;

&lt;p&gt;In &lt;em&gt;Sub-Word Tokenization&lt;/em&gt;, we break text into smaller units that are larger than characters but smaller than full words.&lt;/p&gt;

&lt;p&gt;Key algorithms are Byte-Pair Encoding (BPE), WordPiece and Unigram.&lt;/p&gt;

&lt;h4 id=&quot;byte-pair-encoding&quot;&gt;Byte-Pair Encoding&lt;/h4&gt;

&lt;p&gt;BPE starts with a vocabulary of individual characters and iteratively merges the most frequent pair of symbols until a desired vocabulary size is reached. Merges based on the frequency before adding to the vocabulary.&lt;/p&gt;

&lt;p&gt;Let’s look at how BPE would be used to choose a set of tokens for the string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AAABDAAABAC&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/byte-pair-encoding-basics.png&quot; alt=&quot;Byte-Pair Encoding Basics&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;In the first step, we have just one token ID per character. We then calculate the frequency of each pair of adjacent tokens across the training set. The most frequent pair is merged, and given its own token ID. The training set is updated to use the new token. This process repeats until the desired number of tokens has been generated.&lt;/small&gt;&lt;/p&gt;

&lt;h4 id=&quot;wordpiece&quot;&gt;WordPiece&lt;/h4&gt;

&lt;p&gt;WordPiece is similar to BPE in that it iteratively merges subword units, starting from characters. However, instead of choosing the most frequent pair like BPE, WordPiece uses a probability-based score that approximates how much a new merged symbol would improve the likelihood on the training data.&lt;/p&gt;

&lt;h4 id=&quot;unigram&quot;&gt;Unigram&lt;/h4&gt;

&lt;p&gt;Unigram starts with a large vocabulary of potential sub-words and iteratively removes the least likely ones until the optimal set is found.&lt;/p&gt;

&lt;p&gt;You may also see &lt;a href=&quot;https://github.com/google/sentencepiece&quot;&gt;SentencePiece&lt;/a&gt; mentioned in the context of tokenization. SentencePiece is an unsupervised text tokenizer and de-tokenizer that implements Byte-Pair Encoding (BPE) and the Unigram language model.&lt;/p&gt;

&lt;h3 id=&quot;de-tokenization&quot;&gt;De-Tokenization&lt;/h3&gt;

&lt;p&gt;The output of a language model often depends on the inverse of tokenization. The LM Head of the model is responsible for converting an internal representation of a sentence or document into a series of tokens, often using the same mapping as the original model input. Those tokens are then converted into human-readable text via de-tokenization.&lt;/p&gt;

&lt;p&gt;De-tokenization may be a probabilistic process. If the language model outputs a confidence level for each possible token, then tokens may be sampled according to confidence assigned to them. This can have a huge impact on the quality of the output, and several different approaches are used.&lt;/p&gt;

&lt;h3 id=&quot;greedy-decoding&quot;&gt;Greedy Decoding&lt;/h3&gt;

&lt;p&gt;If we want the fastest output, and can tolerate some mistakes, then the simplest option is Greedy Decoding. We always choose the token with the highest confidence. However, these days Beam Search is more popular.&lt;/p&gt;

&lt;h3 id=&quot;beam-search&quot;&gt;Beam Search&lt;/h3&gt;

&lt;p&gt;Beam Search aims to strike a balance between Greedy Decoding and an exhaustive search for the best possible output sequence. The idea is that we keep a certain number of partial solutions in memory, determined by the beam size &lt;em&gt;K&lt;/em&gt;. At the first time step, we choose the &lt;em&gt;K&lt;/em&gt; most probable tokens, based on the logits generated by the model.&lt;/p&gt;

&lt;p&gt;On all subsequent steps, we extended each candidate sequence by one token, and re-evaluate the joint probability of that particular sequence occurring. All but the top-&lt;em&gt;K&lt;/em&gt; sequences are discarded, and then the process continues. I highly recommend reading &lt;a href=&quot;https://d2l.ai/chapter_recurrent-modern/beam-search.html&quot;&gt;Chapter 10.8 - Beam Search&lt;/a&gt; from &lt;a href=&quot;https://d2l.ai/index.html&quot;&gt;Dive Into Deep Learning&lt;/a&gt; to learn more about the inner workings of Beam Search.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/beam-search.png&quot; alt=&quot;Beam Search&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: &lt;a href=&quot;https://d2l.ai/chapter_recurrent-modern/beam-search.html&quot;&gt;Chapter 10.8 - Beam Search&lt;/a&gt; from &lt;a href=&quot;https://d2l.ai/index.html&quot;&gt;Dive Into Deep Learning&lt;/a&gt;.&lt;br /&gt;This diagram shows a beam size of 2 and maximum sequence length of 3. Greedy search can be treated as a special case of Beam Search where the beam size is set to 1.&lt;/small&gt;&lt;/p&gt;

&lt;h2 id=&quot;rkllm-toolkit&quot;&gt;RKLLM Toolkit&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/airockchip/rknn-llm&quot;&gt;RKLLM-Toolkit&lt;/a&gt; is a software development kit published by Rockchip, that supports model conversion and quantization on a desktop PC. It also includes a device runtime with C/C++ APIs for Rockchip NPUs, making it possible to deploy language models to Rockchip devices.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/rkllm-toolkit.jpg&quot; alt=&quot;RKLLM Toolkit&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: &lt;a href=&quot;https://github.com/airockchip/rknn-llm&quot;&gt;RKLLM-Toolkit&lt;/a&gt; repository on GitHub.&lt;/small&gt;&lt;/p&gt;

&lt;h3 id=&quot;getting-started-with-rkllm&quot;&gt;Getting started with RKLLM&lt;/h3&gt;

&lt;p&gt;Khadas makes it very easy to get started with LLM demos on the Edge2. While the &lt;a href=&quot;https://docs.khadas.com/products/sbc/edge2/npu/llm-on-edge2&quot;&gt;documentation&lt;/a&gt; is a bit light, there isn’t actually that much to it.&lt;/p&gt;

&lt;p&gt;Assuming you’re running a Linux distribution on your Edge device, you should find a script named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;khadas_llm.sh&lt;/code&gt; in your path. Running this will install dependencies, clone the &lt;a href=&quot;https://github.com/khadas/rknn-llm&quot;&gt;rknn-llm&lt;/a&gt; repo and prompt you download/builds models.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;CMake already installed, skipping installation
Repository exists, skipping clone

======================
System Memory: 15.5G
Available Models:
 1) DeepSeek 1.5B        [Missing]
 2) DeepSeek 7B          [Missing]
 3) Qwen2 2B-VL          [Missing]
 4) ChatGLM3 6B          [Missing]
======================
Download missing models? [y/N]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Unfortunately, the first time I tried this, the models failed to download due to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;404 Not Found&lt;/code&gt; errors. I eventually found the correct files on &lt;a href=&quot;https://dl.khadas.com/resources/development&quot;&gt;dl.khadas.com&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/tiny-language-models/khadas-llm-resources.png&quot; alt=&quot;Khadas LLM Resources&quot; class=&quot;shadow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;From here, I simply modified the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;khadas_llm.sh&lt;/code&gt; script, making the following replacement:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dl.khadas.com/development -&amp;gt; dl.khadas.com/resources/development
                                           ^^^^^^^^^
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After doing this, it was smooth sailing!&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Available downloads:
 1) DeepSeek 1.5B
 2) DeepSeek 7B
 3) Qwen2 2B-VL
 4) ChatGLM3 6B
Select model to download: 1
Downloading DeepSeek 1.5B...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The model we want is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1) DeepSeek 1.5B&lt;/code&gt;. The full name for this model is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeepSeek-R1-Distill-Qwen-1.5B&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;NOTE: the order may be different if you run this script on your own device.&lt;/p&gt;

&lt;h3 id=&quot;deepseek-r1-distill-qwen&quot;&gt;DeepSeek-R1-Distill-Qwen&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeepSeek-R1-Distill-Qwen-*&lt;/code&gt; is a series of open-weight reasoning-focused language models where DeepSeek’s larger &lt;a href=&quot;https://huggingface.co/deepseek-ai/DeepSeek-R1&quot;&gt;R1 reasoning model&lt;/a&gt; has been distilled into a Qwen 2.5–based backbones. This model is available in 1.5B, 7B, 14B, 32B parameter variations. We use the &lt;a href=&quot;https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B&quot;&gt;1.5B parameter model&lt;/a&gt; in the example below.&lt;/p&gt;

&lt;p&gt;When you first download the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeepSeek-R1-Distill-Qwen-1.5B&lt;/code&gt; model, it will be stored in a generic format such as &lt;a href=&quot;https://onnx.ai/&quot;&gt;ONNX&lt;/a&gt;. Certain conversion steps need to take place before running on the Rockchip NPU:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Converting the model graph to use Rockchip-compatible operations&lt;/li&gt;
  &lt;li&gt;Layer fusion, which is a kind of optimisation&lt;/li&gt;
  &lt;li&gt;Quantization, to reduce the precision of model weights to fit within device constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is called building the model. We covered these in my &lt;a href=&quot;https://tristanpenman.com/blog/posts/2025/07/20/edge-ai-using-the-rockchip-npu/&quot;&gt;previous post&lt;/a&gt; on Edge AI.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;khadas_llm.sh&lt;/code&gt; script can do this for you automatically. Simply follow the prompts to build the model:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Building DeepSeek 1.5B model...
-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /home/khadas/rknn-llm/examples/DeepSeek-R1-Distill-Qwen-1.5B_Demo/deploy/build/build_linux_aarch64_Release
[100%] Built target llm_demo
[100%] Built target llm_demo
Install the project...
-- Install configuration: &quot;Release&quot;
-- Up-to-date: /home/khadas/rknn-llm/examples/DeepSeek-R1-Distill-Qwen-1.5B_Demo/deploy/install/demo_Linux_aarch64/./llm_demo
-- Up-to-date: /home/khadas/rknn-llm/examples/DeepSeek-R1-Distill-Qwen-1.5B_Demo/deploy/install/demo_Linux_aarch64/lib/librkllmrt.so

Configure runtime parameters (press Enter for defaults)
Max new tokens (default 2048):
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;example-prompt&quot;&gt;Example Prompt&lt;/h3&gt;

&lt;p&gt;Finally, we can enter some text to see how the model responds. Let’s try asking how many planets are in the solar system…&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;user: How many planets are there in the solar system?
robot: &amp;lt;think&amp;gt;
Okay, so I need to figure out how many planets there are in our solar system. Hmm, let me start
by recalling what I know about the solar system. I remember that it&apos;s a system of eight planets
orbiting the sun. But wait, isn&apos;t there something called Pluto as well? I think it was considered
a planet for a long time but later it was classified as a dwarf planet because its size is too
big and it doesn&apos;t have an atmosphere.

So, if there are eight planets in total, including Pluto, then excluding Pluto, how many would be
left? Let me count: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune. That&apos;s
eight. But wait, I think sometimes people include Pluto as a planet too, so maybe the count is
nine.

I should also consider if there are any other planets besides these eight. From what I know, our
solar system has eight planets: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, and Neptune.
There aren&apos;t any others in our galaxy or beyond it that we can observe as planets right now. So,
excluding dwarf planets like Pluto, there are eight.

I wonder if there&apos;s a different way to count them. Maybe including some gas giants or something
else? No, I don&apos;t think so. Each planet is distinct and has its own orbit around the sun without
overlapping with others in terms of size or distance from it. So, each one is unique in their
characteristics.

Wait, could there be any other planets that are not considered planets by modern definitions?
I&apos;m pretty sure all eight are recognized as planets now. There&apos;s also the asteroid belt and the
Kuiper Belt, but those aren&apos;t planets themselves; they&apos;re regions of space with many asteroids.

So, putting it all together, if we count each planet individually, there are eight in our solar
system. If we include Pluto, then it&apos;s nine. But since Pluto is no longer recognized as a planet,
I think the correct answer is eight.
&amp;lt;/think&amp;gt;

There are 8 planets in our solar system. These are Mercury, Venus, Earth, Mars, Jupiter, Saturn,
Uranus, and Neptune. Pluto was once considered a planet but is now classified as a dwarf planet
due to its size and lack of atmosphere.

**Answer:** There are 8 planets in the solar system.

--------------------------------------------------------------------------------------
 Stage         Total Time (ms)  Tokens    Time per Token (ms)      Tokens per Second
--------------------------------------------------------------------------------------
 Prefill       318.77           14        22.77                    43.92
 Generate      33152.13         482       69.91                    14.30
--------------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;next-steps&quot;&gt;Next Steps…&lt;/h2&gt;

&lt;h3 id=&quot;machine-translation&quot;&gt;Machine Translation&lt;/h3&gt;

&lt;p&gt;This content in this post started as just one section of a deep-dive into &lt;em&gt;Machine Translation at the Edge&lt;/em&gt;, which looks at implementing a machine translation system on the RK3588 NPU. Keep an eye out for that post!&lt;/p&gt;

&lt;h3 id=&quot;tokenization-deep-dive&quot;&gt;Tokenization Deep Dive&lt;/h3&gt;

&lt;p&gt;While writing this, I also found myself writing at length about different tokenisation methods, and the latest developments in token-free language models. That content has also been moved to a separate draft, and I hope to have both posts published by the end of the year.&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;p&gt;As usual, I drew upon dozens of resources while preparing this post. The list below contains a selection of links that I think will be useful to someone wanting to dive deeper into the topics covered in this post. They’re listed roughly in the same order as the topics were covered.&lt;/p&gt;

&lt;p&gt;Word Embeddings&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://jalammar.github.io/illustrated-word2vec/&quot;&gt;The Illustrated Word2Vec&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Attention&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.3blue1brown.com/topics/neural-networks&quot;&gt;3blue1brown Neural Networks Course&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GPT&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf&quot;&gt;Language Models are Unsupervised Multitask Learners&lt;/a&gt; - This is the paper that introduced GPT-2&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2005.14165&quot;&gt;Language Models are Few-Shot Learners&lt;/a&gt; - This is the paper that introduced GPT-3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tokenization&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=TGZfZVuF9Yc&quot;&gt;Hugging Face - Unigram Tokenization&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=RAIUJ3VFXmI&quot;&gt;Mastering Tokenization in NLP: The Ultimate Guide to Unigram and Beyond!&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=zduSFxRajkE&quot;&gt;Let’s build the GPT Tokenizer&lt;/a&gt; - Andrei Karpathy’s talk on BPE&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Text Generation&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://d2l.ai/chapter_recurrent-modern/beam-search.html&quot;&gt;Dive Into Deep Learning - Beam Search&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
					<pubDate>Sun, 07 Dec 2025 00:00:00 +0000</pubDate>
					<link>https://tristanpenman.com/blog/posts/2025/12/07/tiny-language-models/</link>
					<guid isPermaLink="true">https://tristanpenman.com/blog/posts/2025/12/07/tiny-language-models/</guid>
				</item>
			
		
			
				<item>
					<title>Edge AI using the Rockchip NPU </title>
					
					<description>&lt;p&gt;Ever wondered what it takes to run real-time object detection and face recognition on a tiny device, without the cloud? I’ve been exploring this recently, while testing the limits of the &lt;a href=&quot;https://www.khadas.com/edge2&quot;&gt;Khadas Edge2&lt;/a&gt;. Powered by a Rockchip RK3588 processor - with a built in neural processing unit - this little &lt;em&gt;Edge AI&lt;/em&gt; device packs quite a punch. And it’s all neatly packaged for consumers and hobbyists alike.&lt;/p&gt;

&lt;p&gt;This post is split into three parts. &lt;a href=&quot;#part-1-edge-ai&quot;&gt;Part 1&lt;/a&gt; will begin with a brief tour of the Edge AI problem space, the kinds of AI models that are available, and technical details of the Edge2. In &lt;a href=&quot;#part-2-software-and-datasets&quot;&gt;Part 2&lt;/a&gt;, we’ll dive into the software and example code available for Rockchip devices, and explore some popular computer vision datasets. Finally, &lt;a href=&quot;#part-3-a-custom-object-detector&quot;&gt;Part 3&lt;/a&gt; will make this more practical, walking through the steps to train a custom object detector.&lt;/p&gt;

&lt;div class=&quot;hidden&quot;&gt;
We&apos;ll avoid going into too much detail about code in this post. Instead we navigate a path through the relevant concepts, resources and tooling. To see the code, check out my [yolov5-experiments](https://github.com/tristanpenman/yolov5-experiments) repo on GitHub.
&lt;/div&gt;

&lt;h3 id=&quot;contents&quot;&gt;Contents&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#part-1-edge-ai&quot;&gt;Part 1: Edge AI&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#example-applications&quot;&gt;Example Applications&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#performance-constraints&quot;&gt;Performance Constraints&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#model-updates&quot;&gt;Model Updates&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#training-vs-inference&quot;&gt;Training vs Inference&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#khadas-edge2&quot;&gt;Khadas Edge2&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#rockchip-rk3588&quot;&gt;Rockchip RK3588&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#power-consumption&quot;&gt;Power Consumption&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#community-support&quot;&gt;Community Support&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#compared-to-nvidia-jetson&quot;&gt;Compared to NVIDIA Jetson&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#low-level-details&quot;&gt;Low Level Details&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#numeric-precision&quot;&gt;Numeric Precision&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#floating-point&quot;&gt;Floating Point&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#memory-architecture&quot;&gt;Memory Architecture&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#applications&quot;&gt;Applications&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#object-detection&quot;&gt;Object Detection&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#facial-recognition&quot;&gt;Facial Recognition&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#language-models&quot;&gt;Language Models&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#sentiment-analysis&quot;&gt;Sentiment Analysis&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#part-2-software-and-datasets&quot;&gt;Part 2: Software and Datasets&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#rknn-toolkit&quot;&gt;RKNN Toolkit&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#model-zoo&quot;&gt;Model Zoo&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#large-language-models&quot;&gt;Large Language Models&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#khadas-resources&quot;&gt;Khadas Resources&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#oowow&quot;&gt;Oowow&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#khadas-examples&quot;&gt;Edge2 Examples&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#quantization&quot;&gt;Quantization&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#calibration&quot;&gt;Calibration&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#asymmetric-quantization&quot;&gt;Asymmetric Quantization&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#gotchas&quot;&gt;Gotchas&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#datasets&quot;&gt;Datasets&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#mnist-and-imagenet&quot;&gt;MNIST and ImageNet&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#vggface2&quot;&gt;VGGFace2&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#coco-dataset&quot;&gt;COCO Dataset&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#open-images-dataset&quot;&gt;Open Images Dataset&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#part-3-transfer-learning&quot;&gt;Part 3: Transfer Learning&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#feature-extraction&quot;&gt;Feature Extraction&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#fine-tuning&quot;&gt;Fine-Tuning&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#yolov5&quot;&gt;YOLOv5&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#source-code&quot;&gt;Source Code&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#pretrained-models&quot;&gt;Pretrained Models&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#performance-metrics&quot;&gt;Performance Metrics&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#head-neck-and-backbone&quot;&gt;Head, Neck and Backbone&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#building-a-dataset&quot;&gt;Building a Dataset&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#fiftyone&quot;&gt;FiftyOne&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#data-preparation&quot;&gt;Data Preparation&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#training-the-model&quot;&gt;Training the Model&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#evaluation&quot;&gt;Evaluation&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#conversion&quot;&gt;Conversion&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#closing-thoughts&quot;&gt;Closing Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;part-1-edge-ai&quot;&gt;Part 1: Edge AI&lt;/h2&gt;

&lt;p&gt;Let’s start from the top.&lt;/p&gt;

&lt;p&gt;You may already be familiar with Cloud AI, thanks to services such as ChatGPT, or even Google Search, with its introduction of AI generated content into search results. If Cloud AI is one end of the AI spectrum, this post focuses on the other end of that spectrum - machine learning algorithms running on local hardware devices. This is generally refered to as Edge AI, or &lt;em&gt;AI at the Edge.&lt;/em&gt; Naturally, there is an appropriately named O’Reilly book on the topic:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/ai-at-the-edge.jpg&quot; alt=&quot;AI at the Edge&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;To learn more about the book, visit &lt;a href=&quot;https://www.oreilly.com/library/view/ai-at-the/9781098120191/&quot;&gt;the O’Reilly website&lt;/a&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Some commonplace examples of Edge AI devices include modern mobile phones, smart appliances, and cars with self-driving or driver-assist features. These are cases where a reliable internet connection may not be available, or safety concerns require that AI processing happens with low latency.&lt;/p&gt;

&lt;h3 id=&quot;example-applications&quot;&gt;Example Applications&lt;/h3&gt;

&lt;p&gt;Edge AI isn’t limited to the examples above. You’ll also find AI in the following embedded settings:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Smart Cameras: Detecting intruders or anomalies without needing to stream footage to the cloud.&lt;/li&gt;
  &lt;li&gt;Voice Control: Processing voice commands locally (e.g., wake word detection).&lt;/li&gt;
  &lt;li&gt;Manufacturing: Identifying defects on the production line.&lt;/li&gt;
  &lt;li&gt;Retail: Monitoring customer foot traffic and shelf inventory in real time.&lt;/li&gt;
  &lt;li&gt;Healthcare: Tracking bio-signals locally for faster diagnosis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With all these examples, it’s not difficult to find some common themes as to why Edge AI can be desireable:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Being able to react to data or signals in real-time, without needing to send data to the cloud. Lower latency means faster, more responsive UX.&lt;/li&gt;
  &lt;li&gt;Offline operation, independent of an internet connection. This means Edge AI works even when you’re on a flight, or stuck in a tunnel.&lt;/li&gt;
  &lt;li&gt;Preservation of privacy. Personal data never leaves the device, which can be critical for privacy-focused users, or heavily regulated industries such as healthcare.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;constraints&quot;&gt;Constraints&lt;/h3&gt;

&lt;p&gt;A key challenge with Edge AI is that devices often have constrained compute, memory, and power budgets that inherently restrict model complexity. This is especially true in the Embedded Systems space. In the most extreme cases, Edge AI may need to run on microprocessor devices such as the Arduino. Of course, it’s common these days to use more powerful alternatives, such as the &lt;a href=&quot;https://docs.arduino.cc/hardware/portenta-h7/&quot;&gt;Arduino Portenta H7&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/arduino-portenta-h7.png&quot; alt=&quot;Arduino Portenta H7&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: &lt;a href=&quot;https://store.arduino.cc/products/portenta-h7&quot;&gt;The Arduino store&lt;/a&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The Portenta H7 features an STM32H747 processor, with two cores - a Cortex-M7 and a Cortex-M4. The separation of these cores means that the device can handle real time control and AI processing simultaneously. However, this processor is &lt;em&gt;not&lt;/em&gt; designed for compute-heavy AI tasks such as object detection and real-time video analysis.&lt;/p&gt;

&lt;h3 id=&quot;model-updates&quot;&gt;Model Updates&lt;/h3&gt;

&lt;p&gt;The embedded nature of Edge AI devices also leads to difficulties in shipping model updates. Regulatory constraints may require that models are trained and rigorously tested up-front. And once a device is deployed, it may be costly (or even illegal) to update the model. Bandwidth and storage can also be an issue. Some state of the art (SOTA) models may be hundreds of megabytes in size (or even gigabytes), making distribution impractical.&lt;/p&gt;

&lt;h3 id=&quot;training-vs-inference&quot;&gt;Training vs Inference&lt;/h3&gt;

&lt;p&gt;Implementation of a machine learning algorithm can be broken down into two key phases: &lt;em&gt;Training&lt;/em&gt; and &lt;em&gt;Inference&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;During the training phase, a machine learning algorithm is exposed to a dataset from which it can extract (or ‘learn’) patterns and other meaningful relationships. The output of training are the parameters for a model, e.g. connection weights for a neural network. Training tends to be computationally expensive, and requires access to training data. On each pass through the dataset (known as an ‘epoch’), the network predicts the labels associated with each example in the dataset. Those predictions are compared with the true labels, and the error is distributed across the weights in the network:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/training.png&quot; alt=&quot;Training&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In the inference phase, the parameters (or weights) learned in the training phase are loaded into memory, and used by the model to process previously unseen inputs. The weights are rarely altered at runtime.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/inference.png&quot; alt=&quot;Inference&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This storage cost for training data is prohibitive for Edge AI applications, and the data itself may be proprietary. As such, Edge AI applications typically focus on efficiency for inference, rather than training.&lt;/p&gt;

&lt;h2 id=&quot;khadas-edge2&quot;&gt;Khadas Edge2&lt;/h2&gt;

&lt;p&gt;This takes us to the Khadas Edge2.&lt;/p&gt;

&lt;p&gt;The Edge2 is a slim and compact ARM PC that is designed with Pogo Pads, FPC headers and MIPI-Interfaces which makes it convenient for developers to access the necessary I/O for embedded applications. It includes HDMI output, two USB-A ports, and two USB-C ports, for data and power:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/khadas-edge2.png&quot; alt=&quot;Khadas Edge2&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The MIPI ports on the bottom edge of the board allow three cameras to be connected directly to the device, for advanced video processing tasks.&lt;/p&gt;

&lt;h3 id=&quot;rockchip-rk3588&quot;&gt;Rockchip RK3588&lt;/h3&gt;

&lt;p&gt;The chip that powers all of this is the Rockchip RK3588S2, an 8-core 64-bit ARM processor. This includes a dedicated NPU (neural processing unit), making it possible to run deep learning inference on-device, without needing a GPU or access to a Cloud AI service. This makes the RK3588 ideal for real world Edge AI applications such as surveillance, robotics, or smart sensors.&lt;/p&gt;

&lt;p&gt;The Rockchip NPU has direct access to system memory, and supports INT8, INT16 and FP16 precision models natively. We’ll discuss the importance of precision further in the &lt;a href=&quot;#numeric-precision&quot;&gt;Numeric Precision&lt;/a&gt; section below.&lt;/p&gt;

&lt;h3 id=&quot;power-consumption&quot;&gt;Power Consumption&lt;/h3&gt;

&lt;p&gt;A critical characteristic of embedded devices is power consumption (along with it’s first cousin, &lt;em&gt;heat dissipation&lt;/em&gt;). Embedded devices are often battery-powered, or deployed in locations where it is difficult to provide active cooling.&lt;/p&gt;

&lt;p&gt;The Edge2 balances strong performance with very modest power consumption. While under load, Thermal Design Power (TDP) is estimated to be between 5-10W. This is especially appealing when compared to using a desktop GPU, which can easily have a TDP 30-50x greater. The NPU can handle up to 6 TOPS (Trillions of Operations Per Second) - very impressive given the power budget.&lt;/p&gt;

&lt;p&gt;Khadas’ documentation recommends a 24W power supply, which will provide ample headroom for powering connected peripherals.&lt;/p&gt;

&lt;h3 id=&quot;community-support&quot;&gt;Community Support&lt;/h3&gt;

&lt;p&gt;On the software side, Rockchip supports several options for deploying models to the device - these include &lt;a href=&quot;https://github.com/airockchip/rknn-toolkit2/&quot;&gt;RKNN Toolkit&lt;/a&gt;, ONNX, and LiteRT (“Lite Runtime”, formerly known as TensorFlow Lite). RKNN Toolkit provides tooling for converting various model formats into &lt;em&gt;RKNN Format&lt;/em&gt;, which run natively on the RK3588 NPU.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/rknn-toolkit-framework.png&quot; alt=&quot;RKNN Toolkit Framework&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image credit: &lt;a href=&quot;https://github.com/airockchip/rknn-toolkit2/&quot;&gt;RKNN Toolkit&lt;/a&gt; documentation.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;This means you can build and train models in popular frameworks (e.g. PyTorch or TensorFlow) before converting them to run natively on the Rockchip NPU. Rockchip maintains an extensive &lt;a href=&quot;https://github.com/airockchip/rknn_model_zoo&quot;&gt;RKNN Model Zoo&lt;/a&gt; on GitHub, which includes example code for many popular models.&lt;/p&gt;

&lt;p&gt;Finally, Khadas provides &lt;a href=&quot;https://docs.khadas.com/products/sbc/edge2/pages&quot;&gt;extensive documentation&lt;/a&gt; for working with the Edge2 itself, and growing community interest around Rockchip devices on Reddit - see the &lt;a href=&quot;https://www.reddit.com/r/RockchipNPU/&quot;&gt;r/RockchipNPU&lt;/a&gt; subreddit.&lt;/p&gt;

&lt;h3 id=&quot;compared-to-nvidia-jetson&quot;&gt;Compared to NVIDIA Jetson&lt;/h3&gt;

&lt;p&gt;Many developers are discovering that these RK3588 boards compare very favourably against NVIDIA Jetson or Raspberry Pi for Edge AI inference. The Khadas Edge2 offers competitive edge AI performance at a lower price point than NVIDIA Jetson Xavier NX or Orin.&lt;/p&gt;

&lt;p&gt;Multiple I/O options, including M.2, USB-C, GPIO, and HDMI, make the Edge2 suitable for integrating into smart cameras, drones, or custom enclosures.&lt;/p&gt;

&lt;h2 id=&quot;low-level-details&quot;&gt;Low Level Details&lt;/h2&gt;

&lt;p&gt;Let’s dive deeper into the capabilities of the device, and how inference on an embedded device is different to other environments.&lt;/p&gt;

&lt;h3 id=&quot;floating-point&quot;&gt;Floating Point&lt;/h3&gt;

&lt;p&gt;When talking about floating points numbers, or ‘floats’, we usually refer to decimal numbers that are represented using 32-bits. These bits can be broken down into three parts: a &lt;em&gt;sign&lt;/em&gt;, an &lt;em&gt;exponent&lt;/em&gt;, and a &lt;em&gt;fraction&lt;/em&gt; (the ‘mantissa’). This is in accordance with the &lt;a href=&quot;https://en.wikipedia.org/wiki/IEEE_754&quot;&gt;IEEE 754 standard&lt;/a&gt;, which describes how these bits can be interpreted as a decimal.&lt;/p&gt;

&lt;p&gt;It’s beyond the scope of this post to explain how all this works. The most intuitive explanation I’ve seen for this is Fabien Sanglard’s &lt;a href=&quot;https://fabiensanglard.net/floating_point_visually_explained/&quot;&gt;Floating Point Visually Explained&lt;/a&gt;. And for much more detail, I recommend &lt;a href=&quot;https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html&quot;&gt;What Every Computer Scientist Should Know About Floating-Point Arithmetic&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;numeric-precision&quot;&gt;Numeric Precision&lt;/h3&gt;

&lt;p&gt;The RK3588 SoC has a dedicated NPU that natively supports models with INT8, INT16 and FP16 numeric precision. These refer to 8-bit integer, 16-bit integer, and 16-bit floating point precision, respectively. These are pretty self-explanatory - INT8 is a signed integer, represented using 8 bits. This means it can take values between -128 and 127. Naturally, INT16 is 16 bits, covering a range between -32768 and 32767.&lt;/p&gt;

&lt;p&gt;There are several numeric representations commonly used in Machine Learning:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/int8-vs-floating-point.png&quot; alt=&quot;INT8 vs Floating Point&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This determines the numerical precision of the weights and activations used during inference, but it is generally NOT the same as the precision used during training. What usually happens is that a model is trained at a higher level of precision (e.g. FP32 or 32-bit floating point) on a desktop or cloud GPU, before being converted for use on the target device.&lt;/p&gt;

&lt;h3 id=&quot;memory-architecture&quot;&gt;Memory Architecture&lt;/h3&gt;

&lt;p&gt;This is another area where the Edge2 differs from a desktop GPU. The Edge2 comes with up to 16GB of LPDDR4 RAM, which is shared by the CPU, GPU, and NPU. This shared memory architecture means data can be easily shared between different processing units, without the overhead of transferring memory from one device to another.&lt;/p&gt;

&lt;p&gt;By comparison, the RAM for an NVIDIA desktop GPU is independent of main system memory. While NVIDIA offers a ‘Unified Memory’ solution that allows system memory and GPU memory to share the same address space, this is simply a convenience for the programmer - it does not avoid the need to transfer data between physical devices.&lt;/p&gt;

&lt;h2 id=&quot;applications&quot;&gt;Applications&lt;/h2&gt;

&lt;p&gt;Now that we know a bit about the kind of device we’re working with, what are some applications that are well suited to it?&lt;/p&gt;

&lt;h3 id=&quot;object-detection&quot;&gt;Object Detection&lt;/h3&gt;

&lt;p&gt;Object Detection is a classic computer vision task that involves identifying, classifying and locating objects within an image or video feed. The key questions that object detection algorithms (attempt to) answer are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;How many objects are present? (Identification)&lt;/li&gt;
  &lt;li&gt;What kinds of object are they? (Classification)&lt;/li&gt;
  &lt;li&gt;Where are they are located? (Localisation)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/object-detection.jpg&quot; alt=&quot;Object Detection&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: Output from Khadas example code for &lt;a href=&quot;https://github.com/khadas/edge2-npu/tree/master/C%2B%2B/ssd&quot;&gt;SSD Object Detection&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Some common models for Object Detection include &lt;a href=&quot;https://paperswithcode.com/method/ssd&quot;&gt;SSD&lt;/a&gt; (Single Shot Detector), &lt;a href=&quot;https://paperswithcode.com/method/retinanet&quot;&gt;RetinaNet&lt;/a&gt;, and &lt;a href=&quot;https://paperswithcode.com/method/faster-r-cnn&quot;&gt;Faster R-CNN&lt;/a&gt;. Another popular choice is YOLO (short for “You Only Look Once”), which we’ll examine in more detail in the second part of this post.&lt;/p&gt;

&lt;h3 id=&quot;facial-recognition&quot;&gt;Facial Recognition&lt;/h3&gt;

&lt;p&gt;Facial Recognition is a classic computer vision problem, generally broken down into two distinct tasks:&lt;/p&gt;

&lt;p&gt;The first is &lt;em&gt;Face Detection&lt;/em&gt;, where we would like to locate all of the faces in an image, similar to how Object Detection is primarily concerned with locating objects.&lt;/p&gt;

&lt;p&gt;The second is &lt;em&gt;Face Recognition&lt;/em&gt;, which has a range of additional applications, such as providing biometric security for personal devices. As you would expect, there are many models available. Just to list a few:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;FaceNet (Google, 2015)&lt;/strong&gt; - This model introduced the concept of face embeddings using triplet loss. The model is trained using example ‘triplets’, each consisting of three separate face images. FaceNet provides a good performance baseline, however newer models may offer superior embedded performance.&lt;/p&gt;

    &lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/triplet-learning.png&quot; alt=&quot;Triplet Learning&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
    &lt;p&gt;&lt;small&gt;Triplet Learning: Positive examples move closer to the anchor, negative examples move further away.&lt;/small&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;RetinaFace&lt;/strong&gt; - RetinaFace builds upon the RetinaNet object detection model, adding the ability to detect human faces and predict key facial landmarks (eyes, nose, corners of the mouth).&lt;/p&gt;

    &lt;p&gt;See the original paper &lt;a href=&quot;https://arxiv.org/abs/1905.00641&quot;&gt;RetinaFace: Single-stage Dense Face Localisation in the Wild&lt;/a&gt; for more detail.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;ArcFace&lt;/strong&gt; or &lt;strong&gt;MagFace&lt;/strong&gt; - ArcFace and MagFace are two newer state-of-the-art models for Face Recognition.&lt;/p&gt;

    &lt;p&gt;ArcFace (2018) introduces a variation of the Softmax Loss function (often referred to referred to as Cross Entropy Loss), which encourages same-class samples to have embeddings closer to one another, while different-class samples are further apart. What this means in practice is tighter clustering of the same person’s face, and clearer separation between different faces.&lt;/p&gt;

    &lt;p&gt;MagFace (2021) builds on ArcFace by incorporating the magnitude of each embedding to the quality of the input face image. Higher-quality face images yield larger embedding magintudes. This allows MagFace to not only maintain strong recognition performance but also provide a built-in measure of face quality, which can be valuable in real-world applications.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;sentiment-analysis&quot;&gt;Sentiment Analysis&lt;/h3&gt;

&lt;p&gt;Sentiment Analysis typically refers to the identification of tone, attitude or emotion present in text or speech. For example, in an e-commerce setting, we could classify online reviews as positive, neutral or negative. For audio or speech, Sentiment Analysis can be used to detect emotional states (e.g. delight, stress or frustration).&lt;/p&gt;

&lt;p&gt;When analysing recordings of speech, sentiment can be inferred from acoustic features. These include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Pitch&lt;/li&gt;
  &lt;li&gt;Loudness&lt;/li&gt;
  &lt;li&gt;Tone of voice&lt;/li&gt;
  &lt;li&gt;Pauses and rhythm of speech&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These may be derived from a Mel Spectrogram:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/spectrogram.png&quot; alt=&quot;Spectrogram&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: Spectrogram of the spoken words “nineteenth century” from &lt;a href=&quot;https://en.wikipedia.org/wiki/Spectrogram&quot;&gt;Wikipedia&lt;/a&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;A Mel Spectrogram has the frequency axis converted to the &lt;a href=&quot;https://en.wikipedia.org/wiki/Mel_scale&quot;&gt;mel scale&lt;/a&gt;, which is designed to match human auditory perception.&lt;/p&gt;

&lt;p&gt;Analysis of text tends to be more subtle, as it may not capture emotional nuances. This can be improved by adding more context. Alternatively, more samples may be required to accurately determine the tone of a body of text.&lt;/p&gt;

&lt;h2 id=&quot;part-2-software-and-datasets&quot;&gt;Part 2: Software and Datasets&lt;/h2&gt;

&lt;p&gt;Now that we have the lay of the land, let’s see what tools are available for the Edge2 specifically, starting with libraries and tooling from Rockchip.&lt;/p&gt;

&lt;h3 id=&quot;rknn-toolkit&quot;&gt;RKNN Toolkit&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/airockchip/rknn-toolkit2/&quot;&gt;RKNN-Toolkit2&lt;/a&gt; is an SDK from Rockchip that supports model conversion, inference and performance evaluation on PC and Rockchip NPU platforms. On the PC side, it supports model conversion and quantization workflows. On the device side, it facilitates loading and running models on the Rockchip NPU, using C/C++ or Python APIs.&lt;/p&gt;

&lt;p&gt;The primary purpose of the toolkit is to take models that have been trained on PC, and convert them into a format that will work with the the RKNN API. Once a model has been concerted to RKNN format, we run inference on a Rockchip NPU using the RKNN C API or Python API. The toolkit contains several examples written in both C++ and Python.&lt;/p&gt;

&lt;p&gt;It’s worth noting that RKNN models are sensitive to input tensor shapes, memory alignment, and batch sizes. If unsupported features are used, then computation may fall back to the CPU. The easiest way to mitigate this is to use Rockchip’s example code, which is designed with these constraints in mind.&lt;/p&gt;

&lt;h3 id=&quot;model-zoo&quot;&gt;Model Zoo&lt;/h3&gt;

&lt;p&gt;When it comes to example code, Rockchip also provides a Model Zoo. &lt;em&gt;“A model what?”&lt;/em&gt; you may be asking.&lt;/p&gt;

&lt;p&gt;Despite the strange name, this is a simple idea. A Model Zoo is a collection of pretrained machine learning models. These models are trained on standard datasets, and are usually provided for popular frameworks (e.g. PyTorch, TensorFlow, or ONNX).&lt;/p&gt;

&lt;p&gt;There’s a fun way to visualise what a Model Zoo offers, courtesy of &lt;a href=&quot;https://www.asimovinstitute.org/&quot;&gt;The Asimov Institute&lt;/a&gt;. In response to the Cambrian explosion of neural network architectures, they created a poster called the &lt;a href=&quot;https://www.asimovinstitute.org/neural-network-zoo/&quot;&gt;Neural Network Zoo&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/neural-network-zoo.png&quot; alt=&quot;Neural Network Zoo&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Rockchip has published their own &lt;a href=&quot;https://github.com/airockchip/rknn_model_zoo&quot;&gt;RKNN Model Zoo&lt;/a&gt; on GitHub, which captures this in a more practical form. This repo contains pretrained models for a variety of common applications. These models are provided in &lt;a href=&quot;https://en.wikipedia.org/wiki/Open_Neural_Network_Exchange&quot;&gt;ONNX format&lt;/a&gt;, and can be readily converted to device-specific RKNN weights.&lt;/p&gt;

&lt;h3 id=&quot;large-language-models&quot;&gt;Large Language Models&lt;/h3&gt;

&lt;p&gt;Support for Large Language Models (LLMs) has been broken out into a separate module called &lt;a href=&quot;https://github.com/airockchip/rknn-llm&quot;&gt;RKNN-LLM&lt;/a&gt;. While LLMs are less common in Edge AI applications, this is without doubt the fastest growing area of AI, and deploying LLMs in embedded contexts is an area of active development.&lt;/p&gt;

&lt;p&gt;LLMs are still very constrained on devices like the RK3588. RKNN-LLM is mostly in early development and supports small-scale models (e.g., TinyLlama, Qwen-1.8B-Int4). Performance is a far cry from GPT-level performance.&lt;/p&gt;

&lt;h2 id=&quot;khadas-resources&quot;&gt;Khadas Resources&lt;/h2&gt;

&lt;h3 id=&quot;khadas-oowow&quot;&gt;Khadas OOWOW&lt;/h3&gt;

&lt;p&gt;The Edge2 comes built-in with Khadas’ OOWOW service. OOWOW, also known as ‘endless wow’ or &lt;a href=&quot;https://docs.khadas.com/software/oowow/about-oowow-name&quot;&gt;infinity wow&lt;/a&gt;, is a standalone embedded service for cloud OS installation, device maintenance, and more.&lt;/p&gt;

&lt;p&gt;OOWOW will be activated automatically if your device storage is empty, otherwise it can be activated by holding the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FUNCTION&lt;/code&gt; button while pressing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RESET&lt;/code&gt;. Once you have configured network access, you will be able to install one of several Android or Linux-based distribution from the cloud!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/oowow-image-selection.jpeg&quot; alt=&quot;OOWOW Image Selection&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;OOWOW Image Selection&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Installation only takes a few minutes. Khadas provides documentation and sample code for each platform (Android and Linux), so this is a quick and easy way to get started with the Edge2.&lt;/p&gt;

&lt;h3 id=&quot;edge2-examples&quot;&gt;Edge2 Examples&lt;/h3&gt;

&lt;p&gt;Khadas provides a range of C++ and Python examples for the Edge2, which can be found in the &lt;a href=&quot;https://github.com/khadas/edge2-npu&quot;&gt;edge2-npu&lt;/a&gt; repo on GitHub. This includes C++ and Python examples. The examples all depend on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;librknn&lt;/code&gt;, Rockchip’s custom neural network library. Many of the examples also depend on OpenCV (both C++ and Python).&lt;/p&gt;

&lt;p&gt;The C++ examples are much more extensive than those for Python. While there are over ten examples for C++, there are just four for Python.&lt;/p&gt;

&lt;h2 id=&quot;quantization&quot;&gt;Quantization&lt;/h2&gt;

&lt;p&gt;An important part of running a model of the Edge2 is ‘Quantization’ - this is the process by which the precision of model weights is reduced. This can significantly reduce the memory footprint of the model, and ensures that we can take advantage of hardware acceleration available at lower levels of precision. This is typically achieved by applying a ‘scale factor’ that reduces the range of possible values (e.g. -128 to 127 for INT8), then truncating those values to the desired precision.&lt;/p&gt;

&lt;p&gt;For example, a model trained with 32-bit floating point (FP32) weights can be quantised to 16-bit floating point (FP16), or even 8-bit integers (INT8), significantly reducing model size and improving inference efficiency, particularly on edge devices.&lt;/p&gt;

&lt;p&gt;To learn more about the theory of Quantization, I suggest reading Hugging Face’s &lt;a href=&quot;https://huggingface.co/docs/optimum/en/concept_guides/quantization&quot;&gt;Conceptual Guide to Quantization&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;calibration&quot;&gt;Calibration&lt;/h3&gt;

&lt;p&gt;RKNN Toolkit provides tooling to quantise models while converting them to RKNN format. This not only adjusts the weights, but also adjusts the calculations performed throughout the model to compensate for the scaled down ranges.&lt;/p&gt;

&lt;p&gt;Unfortunately, we don’t get this for free. Calibration relies on a selection of sample inputs, which are used to choose the scale factor and zero point values for model weights. The simplest option is Symmetric Quantization, in which the zero point from the original FP range is mapped onto zero in the target (e.g. INT8) range:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/symmetric-quantization.png&quot; alt=&quot;Symmetric Quantization&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Symmetric Quantization: Notice that some distinct FP values map onto the same INT8 value.&lt;br /&gt;There is also a region of wasted INT8 range on the left.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The reason this is suboptimal is that the minimum and maximum values of the FP range must have the same absolute value. To see why this is wasteful, consider a weight that takes on values from -512 to 1024. Although the true minimum is -512, the range must extend all the way to -1024.&lt;/p&gt;

&lt;h3 id=&quot;asymmetric-quantization&quot;&gt;Asymmetric Quantization&lt;/h3&gt;

&lt;p&gt;Asymmetric Quantization addresses this by allowing the zero point to be shifted to the left, or to the right:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/asymmetric-quantization.png&quot; alt=&quot;Asymmetric Quantization&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Asymmetric Quantization: Zero point is shifted by -85 units. More of the INT8 range is mapped to positive FP values.&lt;br /&gt;No unused INT8 values on the left.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The key difference here is that the zero point can be chosen as the midpoint between the minimum and maximum FP values. The resulting INT8 range will then cover the entire FP range.&lt;/p&gt;

&lt;h3 id=&quot;gotchas&quot;&gt;Gotchas&lt;/h3&gt;

&lt;p&gt;Quantization may seem like a great idea, with very few drawbacks. In practice, it can be hard to track down issues with loss of accuracy, or overall performance of the model. RKNN models are sensitive to input tensor shapes, memory alignment, and batch sizes - these can all impact model performance at runtime.&lt;/p&gt;

&lt;p&gt;With YOLOv5 for example, the example code includes some small changes to the underlying model graph. Specifically, a post-processing step is moved outside the main model. This is to address stability issues introduced by quantization.&lt;/p&gt;

&lt;h2 id=&quot;datasets&quot;&gt;Datasets&lt;/h2&gt;

&lt;p&gt;Before we talk about the resources that available for/from Rockchip specifically, it’s worth mentioning some of the datasets that are relevant to Edge AI applications.&lt;/p&gt;

&lt;h3 id=&quot;mnist-and-imagenet&quot;&gt;MNIST and ImageNet&lt;/h3&gt;

&lt;p&gt;MNIST and ImageNet are two of the most well known and battle-tested datasets in the field of computer vision models. MNIST is a dataset containing 70,000 grayscale examples of handwritten digits:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/mnist-examples.jpg&quot; alt=&quot;MNIST Examples&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: MNIST Examples courtesy of &lt;a href=&quot;https://en.wikipedia.org/wiki/File:MnistExamples.png&quot;&gt;Wikipedia&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Performance on MNIST was an important benchmark for early Convolutional Neural Networks (CNNs). Beginning with the ImageNet Large Scale Visual Recognition Challenge in 2012, ImageNet became the defacto standard for measuring performance on image classification tasks. The dataset used for that competition (ImageNet-1K) contains ~1.2 million images, across 1000 classes.&lt;/p&gt;

&lt;p&gt;The full ImageNet dataset contains ~14 million labeled images across ~20,000 categories. Although it was created almost 20 years ago, ImageNet continues to be used for training and benchmarking deep learning models on object detection and image classification tasks.&lt;/p&gt;

&lt;h3 id=&quot;vggface2&quot;&gt;VGGFace2&lt;/h3&gt;

&lt;p&gt;VGGFace2 is a large-scale face recognition dataset. It consists of ~3.3 million images of ~9100 subjects, downloaded from Google Image Search. The dataset has been designed to capture the variations that arise due to age, socioeconomic background, and the setting in which the images are captured:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/vggface2-examples.png&quot; alt=&quot;VGGFace2 Examples&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image credit: Example taken from &lt;a href=&quot;https://ar5iv.labs.arxiv.org/html/1710.08092&quot;&gt;VGGFace2: A dataset for recognising faces across pose and age&lt;/a&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;h3 id=&quot;coco-dataset&quot;&gt;COCO Dataset&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://cocodataset.org/&quot;&gt;COCO&lt;/a&gt; is a large-scale object detection, segmentation, and captioning dataset. It is often used to train and benchmark object detection models, such as YOLO. However the wide range of annotations included in the dataset make it suitable for image segmentation, human pose estimation, and even captioning.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/coco-segmentation.png&quot; alt=&quot;COCO Segmentation&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image credit: Adapted from Figure 1 in &lt;a href=&quot;https://arxiv.org/pdf/1405.0312&quot;&gt;Microsoft COCO: Common Objects in Context&lt;/a&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;You will often find pretrained object detection models that have been trained on the COCO dataset. COCO defines 91 classes in total, but the dataset only uses 80 of those classes. An off-the-shelf pretrained model will readily categorise objects into one of those 80 classes, and we can use &lt;a href=&quot;#transfer-learning&quot;&gt;Transfer Learning&lt;/a&gt; to re-fit this to our own custom datasets. This is cover in more detail below.&lt;/p&gt;

&lt;h3 id=&quot;open-images-dataset&quot;&gt;Open Images Dataset&lt;/h3&gt;

&lt;p&gt;The Open Images Dataset (OID) is another large scale dataset that can be extremely valuable when customising pretrained models. OID contains approximately 9 million images, annotated with image-level labels, object bounding boxes, object segmentation masks, visual relationships, and localised narratives.&lt;/p&gt;

&lt;p&gt;The maintainers of OID have partened with Google to offer a tool called &lt;a href=&quot;https://voxel51.com/docs/fiftyone/&quot;&gt;FiftyOne&lt;/a&gt; that can be used to download subsets of the dataset, as needed for specific tasks. For example, if you wanted to train an object detector to recognise firearms, you could download images (and annotations) for classes such as Handgun, Rifle and Shotgun.&lt;/p&gt;

&lt;h2 id=&quot;part-3-transfer-learning&quot;&gt;Part 3: Transfer Learning&lt;/h2&gt;

&lt;p&gt;We now have the fundamental knowledge required to train our own custom object detector. We will take advantage of Transfer Learning, to avoid the cost and complexity of training a model from scratch.&lt;/p&gt;

&lt;p&gt;Transfer Learning refers to the practice of using a pretrained model, typical trained on a benchmark dataset like ImageNet or COCO, for a different but related task.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/pretrained-model.png&quot; alt=&quot;Pretrained Model&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The two main approaches to Transfer Learning are Feature Extraction and Fine-Tuning. These differ in how they treat the existing layers of the model - specifically, whether they are ‘frozen’ or ‘unfrozen’.&lt;/p&gt;

&lt;h3 id=&quot;feature-extraction&quot;&gt;Feature Extraction&lt;/h3&gt;

&lt;p&gt;In Feature Extraction, we freeze all but the top few pretrained layers. Those top layers are then re-trained from scratch, for the new task. This kind of Transfer Learning is very fast, and is well suited to cases where the target task is similar to the original task, or when the target domain is a subset of the original domain.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/feature-extraction.png&quot; alt=&quot;Feature Extraction&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;It’s worth nothing that when training a classifier, we will typically replace the topmost layer entirely. If the top layer previously had 80 outputs (e.g. corresponding to the 80 classes in COCO), we would replace that with a new layer, ensuring that the new layer has the number of outputs required for our custom task.&lt;/p&gt;

&lt;h3 id=&quot;fine-tuning&quot;&gt;Fine-Tuning&lt;/h3&gt;

&lt;p&gt;Fine-Tuning takes this a step further. During training, we unfreeze all of the pretrained layers and train them along with the new layers on a custom dataset. Fine-Tuning is more effective when the target dataset is large enough or differs significantly from the original domain.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/fine-tuning.png&quot; alt=&quot;Fine Tuning&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In practice, these two approaches are often combined: training begins with pure Feature Extraction, using frozen pretrained weights. Once the model achieves a reasonable level of performance, the pretrained weights are unfrozen, allowing them to adapt to the new task.&lt;/p&gt;

&lt;h2 id=&quot;yolov5&quot;&gt;YOLOv5&lt;/h2&gt;

&lt;p&gt;YOLO, or “You Only Look Once”, is best thought of as a family of object detection models. First introduced in 2015 (original paper &lt;a href=&quot;https://arxiv.org/abs/1506.02640&quot;&gt;here&lt;/a&gt;), YOLO has continued to be improved and iterated upon by multiple researchers. You can identify these iterations by a version number suffix (e.g. YOLOv5, YOLOv8, and so on). Many recent advancements have been driven by Ultralytics - an AI company that has built its reputation around pushing the envelope with YOLO object detection.&lt;/p&gt;

&lt;p&gt;A deep dive into the implementation details of YOLOv5 is beyond the scope of this post. However, if you would like to learn more, a great starting point is &lt;a href=&quot;https://docs.ultralytics.com/yolov5/tutorials/architecture_description/&quot;&gt;Ultralytics YOLOv5 Architecture&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/yolov5-model-structure.png&quot; alt=&quot;YOLOv5 Model Structure&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image credit: A small portion of the YOLOv5 Model Structure from &lt;a href=&quot;https://docs.ultralytics.com/yolov5/tutorials/architecture_description/&quot;&gt;Ultralytics YOLOv5 Architecture&lt;/a&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;h3 id=&quot;source-code&quot;&gt;Source Code&lt;/h3&gt;

&lt;p&gt;The fastest way to get started with YOLOv5 is via the &lt;a href=&quot;https://github.com/ultralytics/yolov5&quot;&gt;yolov5&lt;/a&gt; GitHub repo:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Clone the YOLOv5 repository
git clone https://github.com/ultralytics/yolov5

# Navigate to the cloned directory
cd yolov5

# Install required packages
pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;From here, you can easily run YOLOv5 object detection on your PC. Here are just some of the examples from the yolov5 README file:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# Run inference on a local image file
python detect.py --weights yolov5s.pt --source img.jpg

# Run inference on a local video file
python detect.py --weights yolov5s.pt --source vid.mp4

# Run inference on a screen capture
python detect.py --weights yolov5s.pt --source screen
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--weights&lt;/code&gt; argument tells &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;detect.py&lt;/code&gt; which version of the model to use. There are several to choose from, described below.&lt;/p&gt;

&lt;p&gt;There are other scripts in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yolov5&lt;/code&gt; repo that will be of interest to us:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export.py&lt;/code&gt; - Exports a YOLOv5 PyTorch model to other formats, e.g. ONNX.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;train.py&lt;/code&gt; - Trains a YOLOv5 model on a custom dataset.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export.py&lt;/code&gt; can not export directly to RKNN format. For that, we’ll first need to export a model to ONNX format, then convert that to RKNN format using RKNN Toolkit.&lt;/p&gt;

&lt;h3 id=&quot;pretrained-models&quot;&gt;Pretrained Models&lt;/h3&gt;

&lt;p&gt;Ultralytics provides several pretrained models for YOLOv5. Each is a slightly different variation on the model - larger models have wider layers. The number of parameters is proportional to the &lt;em&gt;Width-Depth Multiple&lt;/em&gt; of the model, which scales the number of layers in the model, as well as the width of those layers:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; Model              Width-Depth Multiple   Parameters   Size (MB)   Speed      mAP@0.5 (COCO val)
------------------+----------------------+------------+-----------+----------+-------------------
 yolov5n (nano)     0.25x                       ~1.9M     ~3.9 MB   fastest    ~28.0
 yolov5s (small)    0.33x                       ~7.5M    ~14.0 MB   fast       ~37.4
 yolov5m (medium)   0.67x                      ~21.8M    ~41.0 MB   medium     ~45.4
 yolov5l (large)    1.00x                      ~46.5M    ~92.0 MB   slower     ~49.0
 yolov5x (xlarge)   1.25x                      ~87.7M   ~166.0 MB   slowest    ~50.7
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The nano and small variations are well suited to rapid iteration in Edge AI and embedded contexts, where memory and compute are at a premium. However as you can see from the table, this comes at the cost of mAP, or Mean Average Precision. It’s worth digging into this in some detail.&lt;/p&gt;

&lt;h3 id=&quot;performance-metrics&quot;&gt;Performance Metrics&lt;/h3&gt;

&lt;p&gt;For object detection models, the metrics we most care about are Mean Average Precision (mAP) and Intersection over Union (IoU). IoU is best understood in graphical form. Given a ground truth bounding box, and a bounding box prediction, we first calculate the area of the intersection and union of those boxes. We then take the ratio of those two values:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/intersection-over-union.png&quot; alt=&quot;Intersection Over Union&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Mean Average Precision is an aggregated metric, that takes IoU into account. Precision answers the question: “Given all the objects the model identified, what proportion of these were actually correct?”&lt;/p&gt;

&lt;p&gt;In the results shown above, mAP50 is the Mean Average Precision at IoU = 0.50. This means a prediction is considered correct if its IoU with a ground truth box is at least 0.50.&lt;/p&gt;

&lt;p&gt;This measurement can be made stricter and more comprehensive, by averaging the mAP over a range of IoU thresholds. The mAP50-95 metric is the Mean Average Precision averaged over IoU thresholds from 0.50 to 0.95. This is calculated in steps of 0.05 (i.e. 0.50, 0.55, …, 0.90, 0.95).&lt;/p&gt;

&lt;h3 id=&quot;head-neck-and-backbone&quot;&gt;Head, Neck and Backbone&lt;/h3&gt;

&lt;p&gt;An important detail of the YOLOv5 model is that it can be separated into three parts:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The Backbone - This is the main body of the model, responsible for extracting details such as patterns and geometric features.&lt;/li&gt;
  &lt;li&gt;The Neck - This takes the information gathered by the backbone, reducing it down by combining and aggregating features.&lt;/li&gt;
  &lt;li&gt;The Head - This uses the information aggregated by the neck to make predictions about objects in the image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation allows us to use Transfer Learning to adapt the model to new tasks. We can take advantage of the pretrained &lt;em&gt;Backbone&lt;/em&gt; and &lt;em&gt;Neck&lt;/em&gt; weights by leaving those frozen - this means they won’t change while we train our custom model. The Head will be replaced with new dense layers, specifically for our custom task. It’s those new layers that will be trained on our custom dataset.&lt;/p&gt;

&lt;p&gt;In order to do that, we need to construct an appropriate training dataset.&lt;/p&gt;

&lt;h2 id=&quot;building-a-dataset&quot;&gt;Building a Dataset&lt;/h2&gt;

&lt;p&gt;To make the most of Transfer Learning, you need a custom dataset with examples that are representative of a new task. These examples must be annotated using YOLO-style annotations for bounding boxes. The YOLO annotation format is very simple:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;class_id&amp;gt; &amp;lt;x_center&amp;gt; &amp;lt;y_center&amp;gt; &amp;lt;width&amp;gt; &amp;lt;height&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;All coordinates are normalized (range 0.0 to 1.0), and the class ID is a numeric index. The advantage of normalized coordinates is that they are independent of a particular image size.&lt;/p&gt;

&lt;h3 id=&quot;fiftyone&quot;&gt;FiftyOne&lt;/h3&gt;

&lt;p&gt;The &lt;a href=&quot;https://storage.googleapis.com/openimages/web/index.html&quot;&gt;Open Images Dataset&lt;/a&gt; (discussed earlier) is a great resource for building custom datasets. The OID website provides instructions on how to download a subset of images and corresponding annotations for your task. This includes an open source tool called &lt;a href=&quot;https://github.com/voxel51/fiftyone&quot;&gt;FiftyOne&lt;/a&gt; from &lt;a href=&quot;https://voxel51.com/&quot;&gt;Voxel51&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/edge-ai-using-the-rockchip-npu/fiftyone.jpg&quot; alt=&quot;FiftyOne&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image Credit: Screenshot from the introductory video included in the &lt;a href=&quot;https://docs.voxel51.com/&quot;&gt;FiftyOne Docs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;One you have created a custom dataset, you should split it into training, validation and test sets. The training dataset will be used to train the model, while the validation dataset will be used to tweak hyperparameters and to choose a base pretrained model. The test set can be used at the end to evaluate overall performance.&lt;/p&gt;

&lt;h3 id=&quot;data-preparation&quot;&gt;Data Preparation&lt;/h3&gt;

&lt;p&gt;Since we’re training a YOLOv5 model, we can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;train.py&lt;/code&gt; from the &lt;a href=&quot;https://github.com/ultralytics/yolov5&quot;&gt;yolov5&lt;/a&gt; GitHub repo. This distribution contains the latest and greatest that Ultralytics has to offer. To train on a custom dataset, we use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--data&lt;/code&gt; argument to provide the path to a YAML file that describes our dataset:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python train.py --img 640 --batch 16 --epochs 20 --data datasets/firearms.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The other arguments here specify the input image size, batch size, and number of training epocs to complete.&lt;/p&gt;

&lt;p&gt;There isn’t much to the dataset YAML file, but there is one detail that can be confusing at first:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# this path is relative to the train.py script
path: ../datasets/firearms
train: images/train
test: images/test
val: images/validation

# Classes
names:
  0: Handgun
  1: Rifle
  2: Shotgun
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The gotcha here is that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;path&lt;/code&gt; is relative to the train.py script. Let’s say you’ve checked out the YOLOv5 source into a directory called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yolov5&lt;/code&gt; and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;firearms&lt;/code&gt; dataset in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;datasets&lt;/code&gt; directory alongside that. Printing out the directory tree using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tree&lt;/code&gt; should look something like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;% tree . -L 3
.
├── datasets
│   ├── firearms
│   │   ├── images
│   │   └── labels
│   └── firearms.yaml
└── yolov5
    ├── benchmarks.py
    ├── CITATION.cff
    ├── classify
    |
    :   // trimmed for brevity //
    |
    ├── train.py
    └── val.py

22 directories, 61 files
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When you set your path, you need to take this directory structure into account.&lt;/p&gt;

&lt;h2 id=&quot;training-the-model&quot;&gt;Training the Model&lt;/h2&gt;

&lt;p&gt;Training with YOLOv5 will print out heaps of interesting information process about the model, and how it is performing after each epoch.&lt;/p&gt;

&lt;p&gt;Assuming you run the same train.py command as above:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python yolov5/train.py &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--img&lt;/span&gt; 640 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--batch&lt;/span&gt; 16 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--epochs&lt;/span&gt; 50 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--data&lt;/span&gt; datasets/firearms.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You should get some output that is similar to this (at the end of the output):&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Validating yolov5/runs/train/exp12/weights/best.pt...
Fusing layers...
Model summary: 157 layers, 7018216 parameters, 0 gradients, 15.8 GFLOPs
                 Class     Images  Instances          P          R      mAP50   mAP50-95
                   all         99        129      0.656      0.714      0.722      0.502
               Handgun         99         14      0.677      0.929      0.856      0.687
                 Rifle         99         89      0.824      0.674      0.796      0.532
               Shotgun         99         26      0.467      0.538      0.515      0.287
Results saved to yolov5/runs/train/exp12
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The output includes mAP@50 scores for each of the classes in the custom dataset (closer to 1 is better), as well as the mAP@50-95 scores. It’s normal for the mAP@50-95 scores to be lower, since they are stricter and more conservative.&lt;/p&gt;

&lt;p&gt;The weights of each run are stored in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yolov5/run/trains/exp&amp;lt;run&amp;gt;&lt;/code&gt;. You can see above that the output is from run 12, since the path is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yolov5/runs/train/exp12&lt;/code&gt;. After each run, the best weights will be found in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;weights/best.pt&lt;/code&gt;. This model file is in PyTorch format.&lt;/p&gt;

&lt;h3 id=&quot;evaluation&quot;&gt;Evaluation&lt;/h3&gt;

&lt;p&gt;After training, you can evaluate your model using the test set to measure its performance:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python yolov5/val.py &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--task&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--weights&lt;/span&gt; yolov5/runs/train/exp12/weights/best.pt &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--data&lt;/span&gt; datasets/firearms.yaml &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--img&lt;/span&gt; 640
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The results should look like something like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Fusing layers...
Model summary: 157 layers, 7018216 parameters, 0 gradients, 15.8 GFLOPs
                 Class     Images  Instances          P          R      mAP50   mAP50-95
                   all        307        382      0.611       0.57      0.581      0.373
               Handgun        307         57      0.665      0.737      0.698      0.483
                 Rifle        307        259      0.753      0.517      0.677      0.436
               Shotgun        307         66      0.415      0.455      0.368        0.2
Speed: 0.2ms pre-process, 2.8ms inference, 1.7ms NMS per image at shape (32, 3, 640, 640)
Results saved to yolov5/runs/val/exp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice there is a reduction in mAP50 and mAP50-95 scores. This tells us that the model does not fully generalise to examples outside of the training dataset. This can indicate the model is over-fitting the dataset.&lt;/p&gt;

&lt;h3 id=&quot;conversion&quot;&gt;Conversion&lt;/h3&gt;

&lt;p&gt;The final step in this process is to prepare the model to run on the Edge2. This requires two conversion steps:&lt;/p&gt;

&lt;p&gt;First, we must convert the model from PyTorch to ONNX format. ONNX is a neural network interchange format that is supported as an input format by RKNN Toolkit. This is trivial using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export.py&lt;/code&gt; script from Ultralytics YOLOv5:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python yolov5/export.py \
  --data datasets/firearms.yaml \
  --img 640 \
  --include onnx \
  --weights yolov5/runs/train/exp12/weights/best.pt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will write the ONNX model data to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yolov5/runs/train/exp12/weights/best.onnx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Second, we must convert the model to RKNN format, so that it can run natively on a Rockchip NPU. There are many reasons why a model may not run natively on the NPU (e.g., unsupported layer types, tensor shapes, or memory alignment). This is one of the reasons why it is worth exploring the models in the &lt;a href=&quot;https://github.com/airockchip/rknn_model_zoo&quot;&gt;RKNN Model Zoo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The conversion to RKNN format is a little more involved, since we need to provide a calibration dataset for quantization. The calibration dataset is simply a subset of images from the full training set, which can be used to estimate zero points and scale factors during quantization.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python3 rknn_model_zoo/examples/yolov5/python/convert.py \
  yolov5/runs/train/exp12/weights/best.onnx \
  firearms-calib.txt \
  rk3588
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;With native support for INT8/FP16 precision, an efficient NPU, and plenty of examples via RKNN Toolkit, the Rockchip RK3588 offers a well-rounded platform for running real-world AI workloads at the edge. This post has shown that it’s very achieveable to fine-tune and deploy YOLOv5 without needing a server, GPU, or internet connection.&lt;/p&gt;

&lt;p&gt;Edge AI is not without its challenges and constraints. As you get further into converting and deploying your own models on the Edge2, you will discover that Machine Learning demands a high level of care. It’s important to invest time in understanding the nuances of quantization, transfer learning, dataset preparation, and evaluation. These are all essential components in an end-to-end inference deployment.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Where to from here?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My hope is that this post inspires you to experiment with some of your own ML models - even without an Edge2, most of what we’ve explored in this post applies to desktop ML too! As for my own experiments, I’m beginning to explore Machine Translation at the Edge, and what is achievable with Tiny Language Models. Stay tuned for that!&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;p&gt;These are just a handful of references I used while writing up this post. There are many others, and I will update this list as I incorporate feedback.&lt;/p&gt;

&lt;p&gt;YOLO:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&amp;amp;arnumber=8839032&quot;&gt;Mini-YOLOv3: Real-Time Object Detector for Embedded Applications&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;COCO:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.v7labs.com/blog/coco-dataset-guide&quot;&gt;V7: COCO Dataset: All You Need to Know to Get Started&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://someshfengde.medium.com/coco-dataset-what-is-it-and-how-can-we-use-it-e34a5b0c6ecd&quot;&gt;Coco dataset, What is it? and How can we use it?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://paperswithcode.com/dataset/coco&quot;&gt;Papers With Code: COCO (Common Objects in Context)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.ultralytics.com/datasets/detect/coco/&quot;&gt;Ultralytics: COCO Dataset&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quantization:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://huggingface.co/docs/optimum/en/concept_guides/quantization&quot;&gt;Hugging Face: Quantization&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://newsletter.maartengrootendorst.com/p/a-visual-guide-to-quantization&quot;&gt;Maarten Grootendorst: A Visual Guide to Quantization&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://intellabs.github.io/distiller/algo_quantization.html&quot;&gt;Neural Network Distiller: Quantization Algorithms&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Floating Point:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://fabiensanglard.net/floating_point_visually_explained/&quot;&gt;Fabien Sanglard: Floating Point Visually Explained&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ImageNet / ILSVRC:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.image-net.org/challenges/LSVRC/2012/&quot;&gt;ImageNet Large Scale Visual Recognition Challenge 2012 (ILSVRC2012)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
					<pubDate>Sun, 20 Jul 2025 00:00:00 +0000</pubDate>
					<link>https://tristanpenman.com/blog/posts/2025/07/20/edge-ai-using-the-rockchip-npu/</link>
					<guid isPermaLink="true">https://tristanpenman.com/blog/posts/2025/07/20/edge-ai-using-the-rockchip-npu/</guid>
				</item>
			
		
			
				<item>
					<title>Chromecast Device Authentication </title>
					
					<description>&lt;p&gt;Just recently, there was a high profile Chromecast outage, affecting second generation (Chromecast 2) devices. This happened because &lt;a href=&quot;https://www.reddit.com/r/Chromecast/comments/1j7lhrs/the_chromecast_2s_device_authentication/&quot;&gt;The Chromecast 2’s device authentication certificate has expired&lt;/a&gt;. This gave me an excuse to revisit some older content that I never got around to publishing, about how one can spoof the Device Authentication process - the same process that failed here.&lt;/p&gt;

&lt;p&gt;My initial project resulted in a partially-working Chromecast Receiver, written in Go (&lt;a href=&quot;https://github.com/tristanpenman/go-cast&quot;&gt;code here&lt;/a&gt;). This implements enough of the Google Cast protocol to receive screen mirrored content from Chrome. However, this post focuses on a particular aspect of that project, which is how a third party Google Cast implementation can masquerade as a genuine Chromecast receiver.&lt;/p&gt;

&lt;h3 id=&quot;contents&quot;&gt;Contents&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#introduction&quot;&gt;Introduction&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#google-cast-protocol&quot;&gt;Google Cast Protocol&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#senders-and-receivers&quot;&gt;Senders and Receivers&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#protobuf&quot;&gt;Protobuf&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#device-discovery&quot;&gt;Device Discovery&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#multicast-dns&quot;&gt;Multicast DNS&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#device-authentication&quot;&gt;Device Authentication&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#motivation&quot;&gt;Motivation&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#certificates-and-private-keys&quot;&gt;Certificates and Private Keys&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#peer-certificates&quot;&gt;Peer Certificates&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#pki&quot;&gt;PKI&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#ica-certificates&quot;&gt;ICA Certificates&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#challenge-request&quot;&gt;Challenge Request&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#aside-chromecast-outage&quot;&gt;Aside: Chromecast Outage&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#problem-statement&quot;&gt;Problem Statement&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#prior-work&quot;&gt;Prior Work&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#certificate-manifest&quot;&gt;Certificate Manifest&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#tls-server&quot;&gt;TLS Server&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#root-access&quot;&gt;Root Access&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#eureka-rom-firmware&quot;&gt;Eureka-ROM Firmware&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#flashing-process&quot;&gt;Flashing Process&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#gotchas&quot;&gt;Gotchas&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#signature-generation&quot;&gt;Signature Generation&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#chromecast-tools&quot;&gt;Chromecast Tools&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#aside-toolchain-compatibility&quot;&gt;Aside: Toolchain Compatibility&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#mining-certificates&quot;&gt;Mining Certificates&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#references&quot;&gt;References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;If you’ve used a Chromecast before, you may be familiar with the Cast extension in Google Chrome (now built in to the browser). You may have even used the Cast functionality available on an Android device, to mirror your screen or play a video on a Chromecast.&lt;/p&gt;

&lt;p&gt;Or perhaps, you’ve never seen a Chromecast. This is what a second generation Chromecast looks like:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/chromecast-device-authentication/chromecast-v2.png&quot; alt=&quot;Second generation Chromecast&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;While the Chromecast design has been through a number of iterations, the basic concept remains the same. It’s a small USB-powered device, that you connect to your TV via HDMI. Basic capabilities include Wi-Fi, Bluetooth, and hardware accelerated video decoding, all in a very compact package. Chromecast also offers some support third-party apps, which run on a minimal web browser.&lt;/p&gt;

&lt;h2 id=&quot;google-cast-protocol&quot;&gt;Google Cast Protocol&lt;/h2&gt;

&lt;p&gt;The protocol underlying all of this is called &lt;a href=&quot;https://en.wikipedia.org/wiki/Google_Cast&quot;&gt;Google Cast&lt;/a&gt;. This is Google’s proprietary protocol for launching and controlling applications on a Chromecast device, and for sharing video and audio content to Chromecast-compatible devices. While our main focus in this post is device authentication, we’ll touch upon how the Google Cast protocol has been designed to accomodate custom Chromecast apps.&lt;/p&gt;

&lt;p&gt;But first, we should talk about the difference between &lt;strong&gt;Chromecast Senders&lt;/strong&gt; and &lt;strong&gt;Chromecast Receivers&lt;/strong&gt;.&lt;/p&gt;

&lt;h3 id=&quot;senders-and-receivers&quot;&gt;Senders and Receivers&lt;/h3&gt;

&lt;p&gt;As the name suggests, a Chromecast Receiver serves primarily to receive and play video/audio content. It can also be used to mirror the screen of a Chromecast Sender.&lt;/p&gt;

&lt;p&gt;This isn’t all a receiver can do. An official Chromecast also bundles a stripped down web browser, which can be used to display web-based content. This makes the device capable of hosting third-party &lt;em&gt;Applications&lt;/em&gt;. This allows the Chromecast to run third-party apps, which are effectively single page apps that can play video/audio from external sources such as YouTube. Third-party apps are typically controlled from a user’s mobile device, with a control channel established for sending messages back and forth.&lt;/p&gt;

&lt;p&gt;The Google Cast protocol provides namespace and messaging semantics that make all of this work.&lt;/p&gt;

&lt;h3 id=&quot;protobuf&quot;&gt;Protobuf&lt;/h3&gt;

&lt;p&gt;The messages used by the Google Cast protocol are defined using Google’s &lt;a href=&quot;https://github.com/protocolbuffers/protobuf&quot;&gt;Protocol Buffers&lt;/a&gt; data serialisation format, more commonly known as &lt;strong&gt;Protobuf&lt;/strong&gt;. Protobuf takes a single file that describes the serialisation format for a protocol, and uses that to generate language-specific bindings that marshall and un-marshall data in that format.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/chromecast-device-authentication/protobuf-concepts.png&quot; alt=&quot;Protobuf Concepts&quot; class=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Image credit: &lt;a href=&quot;https://protobuf.dev/overview/&quot;&gt;Protobuf Overview&lt;/a&gt; from &lt;em&gt;Protocol Buffers Documentation&lt;/em&gt;.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;At a high level, each Google Cast message includes the following fields:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Namespace&lt;/li&gt;
  &lt;li&gt;Source ID&lt;/li&gt;
  &lt;li&gt;Destination ID&lt;/li&gt;
  &lt;li&gt;Payload (can be binary or a UTF-8 string)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payload can be binary or text (assumed to be JSON formatted). The namespace tells us how to interpret the contents of the payload. The source ID and destination ID are included because a single communication channel may carry messages that are intended for different Chromecast applications.&lt;/p&gt;

&lt;p&gt;You can see how these are defined in Protobuf format in the file &lt;a href=&quot;https://source.chromium.org/chromium/chromium/src/+/main:third_party/openscreen/src/cast/common/channel/proto/cast_channel.proto&quot;&gt;cast_channel.proto&lt;/a&gt; from the Chromium source code. Note that this file is now part of the &lt;a href=&quot;https://chromium.googlesource.com/openscreen/&quot;&gt;Open Screen Library&lt;/a&gt;, which aims to implement the Open Screen Protocol, Multicast DNS, and the Google Cast protocol.&lt;/p&gt;

&lt;h3 id=&quot;device-discovery&quot;&gt;Device Discovery&lt;/h3&gt;

&lt;p&gt;The next topic we should cover is &lt;strong&gt;Device Discovery&lt;/strong&gt;. This is the mechanism by which a Chromecast Sender (e.g. your web browser or Android device) finds Chromecast Receivers on your local network.&lt;/p&gt;

&lt;p&gt;Device Discovery relies on a protocol called &lt;a href=&quot;https://en.wikipedia.org/wiki/Multicast_DNS&quot;&gt;Multicast DNS&lt;/a&gt;, or mDNS for short. As you may guess from the name, Multicast DNS is a variation on the traditional DNS protocol that powers the internet.&lt;/p&gt;

&lt;p&gt;With traditional DNS it is possible for a host to query other services on a network using human-readable domain names (e.g. google.com). For example, when you enter &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tristanpenman.com&lt;/code&gt; into your web browser, DNS is used to find the IP address of the web server that contains the relevant content.&lt;/p&gt;

&lt;p&gt;A limitation of DNS is that it is centralised, in the sense that we must know the IP address of a DNS server ahead of time. The DNS server must also know about devices on the network ahead of time, via manual configuration. Unfortunately, this doesn’t work well in local network scenarios where DNS may not be so easy to manage. The &lt;strong&gt;Multicast DNS&lt;/strong&gt; standard is designed to overcome this limitation.&lt;/p&gt;

&lt;h3 id=&quot;multicast-dns&quot;&gt;Multicast DNS&lt;/h3&gt;

&lt;p&gt;Multicast DNS, or &lt;strong&gt;mDNS&lt;/strong&gt; for short, handles service lookup in a de-centralised fashion. Instead of directing queries to a specific DNS server, queries are broadcast on the local network using a multicast address. These queries request information about particular services available on the network. Broadcast packets are forwarded to all devices on the network, and any device is free to respond to any query.&lt;/p&gt;

&lt;p&gt;To discover a Chromecast, a client will broadcast a query for devices that offer the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_googlecast._tcp&lt;/code&gt; service. Chromecast devices on the local network will then broadcast a response listing their capabilities, and how to connect (IP and port). This is called ‘advertising’ a service. mDNS responses are broadcast to all devices on the local network, so it is possible for clients to discover new Chromecast devices passively, even they are not actively querying for them.&lt;/p&gt;

&lt;p&gt;At startup, a Chromecast Receiver will advertise the existence of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_googlecast._tcp&lt;/code&gt; service, on port 8009. It will also respond to queries for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_googlecast._tcp&lt;/code&gt; so that new Chromecast Senders are able to find it when they are first started.&lt;/p&gt;

&lt;p&gt;Once a Chromecast Sender has discovered a Receiver, it is free to connect to it on port 8009. Connections are secured using TLS, which is an important detail, as it relates to the Device Authentication mechanism that is part of the Google Cast protocol. We’ll come back to this shortly.&lt;/p&gt;

&lt;h2 id=&quot;device-authentication&quot;&gt;Device Authentication&lt;/h2&gt;

&lt;p&gt;A key challenge in implementing a viable Chromecast Receiver is &lt;em&gt;Device Authentication&lt;/em&gt;. This is the mechanism by which a Chromecast Sender determines that a device is a genuine Chromecast, or an officially licensed Chromecast-compatible Receiver.&lt;/p&gt;

&lt;p&gt;The way this works is that a Chromecast Sender issues a Device Authentication challenge immediately after connecting to a Chromecast Receiver. The Receiver is expected to extract the challenge payload, sign it cryptographically using a private key that is unique to the Receiver device, thus producing a valid Device Authentication response.&lt;/p&gt;

&lt;p&gt;Google Cast connections are secured using TLS. At a minimum, this requires that a Chromecast receiver has a self-signed x509 certificate. While this certificate is self-signed, and therefore not part of a traditional PKI certificate chain, the Google Cast protocol provides another means by which this certificate can be verified as belonging genuine Chromecast device. That process is the focus of this post.&lt;/p&gt;

&lt;h3 id=&quot;motivation&quot;&gt;Motivation&lt;/h3&gt;

&lt;p&gt;Let’s start with the basics. We may ask: Why bother verifying the authenticity of a Chromecast Peer Certificate?&lt;/p&gt;

&lt;p&gt;There are several reasons to do this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Google would look to maintain control over the Google Cast ecosystem. By verifying that a Peer Certificate belongs to a genuine Chromecast, or a licensed third-party device, Google can ensure that the availability of devices in the Google Cast ecosystem is well regulated.&lt;/li&gt;
  &lt;li&gt;Users would like to know that the device they are connected to is in fact the device they expect. Without Device Authentication it is possible for an attacker to masquerade as another device on the network, possibly intercepting connections.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a nutshell, a Device Authentication challenge allows a Chromecast Sender to verify that it is connected to a genuine, or officially licensed, Chromecast device, and that the connection has not been intercepted.&lt;/p&gt;

&lt;h3 id=&quot;peer-certificates&quot;&gt;Peer Certificates&lt;/h3&gt;

&lt;p&gt;So how does this Device Authentication mechanism work?&lt;/p&gt;

&lt;p&gt;I mentioned above that Google Cast TLS connections are secured using a self-signed x509 certificate. This certificate, hereafter called the &lt;strong&gt;Peer Certificate&lt;/strong&gt;, is regenerated every 24 hours. On its own, this certificate offers little assurance that a user has connected to a genuine Chromecast device.&lt;/p&gt;

&lt;p&gt;We need three things if we are to work around this limitation:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The Chromecast receiver must be able to sign its Peer Certificate, using a private key that is fused in the CPU / SoC. We call this key the &lt;strong&gt;Device Private Key&lt;/strong&gt;, and it has a corresponding &lt;strong&gt;Device Certificate&lt;/strong&gt;, which can be found in the Chromecast file system.&lt;/li&gt;
  &lt;li&gt;The Chromecast sender must be able to verify that the &lt;em&gt;Peer Certificate&lt;/em&gt; was signed using the &lt;em&gt;Device Private Key&lt;/em&gt;. It can do this using the public key contained in the &lt;em&gt;Device Certificate&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Finally&lt;/em&gt;, the Chromecast Sender must be able to verify the authenticity of the &lt;em&gt;Device Certificate&lt;/em&gt;. i.e. that the &lt;em&gt;Device Certificate&lt;/em&gt; was generated by an entity that is trusted by Google.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the case of an official Chromecast, the &lt;em&gt;Device Private Key&lt;/em&gt; is fused into the Soc (System on a Chip), and cannot be accessed directly by code running on the device. Instead, the CPU provides cryptographic instructions that allow it to sign a small amount of data (e.g a SHA256 hash) using the private key.&lt;/p&gt;

&lt;h3 id=&quot;pki&quot;&gt;PKI&lt;/h3&gt;

&lt;p&gt;The second part of the process - verifying the authenticity of the &lt;em&gt;Device Certificate&lt;/em&gt; - is achieved using &lt;strong&gt;PKI&lt;/strong&gt;, or &lt;a href=&quot;https://en.wikipedia.org/wiki/Public_key_infrastructure&quot;&gt;Public Key Infrastructure&lt;/a&gt;. PKI allows a series of certificates to verified, all the way up to a ‘root’ certificate. Together, the certificates forms a ‘chain of trust’.&lt;/p&gt;

&lt;p&gt;In the case of Chromecast, there are three certificates that are included in this chain. The first is the Device Certificate - unique to the Chromecast device. The second certificate corresponds to an Intermediate Certificate Authority (or ICA). Each Chromecast generation has its own ICA. The third certificate is the Google Root CA Certificate.&lt;/p&gt;

&lt;p&gt;The complete chain looks like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Device Certificate
        |
        |---------------&amp;gt; ICA Certificate
            verified by         |
                                |---------------&amp;gt; Root CA Certificate
                                    verified by
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is a lot to digest, so hopefully a diagram will help…&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/chromecast-device-authentication/device-authentication.png&quot; alt=&quot;Device Authentication&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In this diagram, blue boxes represent information that must be known to the receiver software at runtime. This becomes more relevant when we implement Device Authentication ourselves. We can also see here that the &lt;em&gt;Device Private Key&lt;/em&gt; is kept in a secure enclave, which can only be accessed indirectly by cryptographic CPU instructions.&lt;/p&gt;

&lt;h3 id=&quot;ica-certificates&quot;&gt;ICA Certificates&lt;/h3&gt;

&lt;p&gt;The ICA Certificates are of particular interest, because they are shared across an entire product line. At the time that a Chromecast device is manufactured, it’s fused Private Key is signed using the ICA Private Key corresponding to this certificate.&lt;/p&gt;

&lt;p&gt;We can use OpenSSL command line tools to inspect the ICA certificate:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1 (0x1)
        Signature Algorithm: sha1WithRSAEncryption
        Issuer: C=US, ST=California, L=Mountain View, O=Google Inc, OU=Google TV, CN=Eureka Root CA
        Validity
            Not Before: Dec 19 00:47:12 2012 GMT
            Not After : Dec 14 00:47:12 2032 GMT
        Subject: C=US, ST=California, L=Mountain View, O=Google Inc, OU=Google TV, CN=Eureka Gen1 ICA
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:bc:22:80:bd:80:f6:3a:21:00:3b:ae:76:5e:35:
                    7f:3d:c3:64:5c:55:94:86:34:2f:05:87:28:cd:f7:
                    69:8c:17:b3:50:a7:b8:82:fa:df:c7:43:2d:d6:7e:
                    ab:a0:6f:b7:13:72:80:a4:47:15:c1:20:99:50:cd:
                    ec:14:62:09:5b:a4:98:cd:d2:41:b6:36:4e:ff:e8:
                    2e:32:30:4a:81:a8:42:a3:6c:9b:33:6e:ca:b2:f5:
                    53:66:e0:27:53:86:1a:85:1e:a7:39:3f:4a:77:8e:
                    fb:54:66:66:fb:58:54:c0:5e:39:c7:f5:50:06:0b:
                    e0:8a:d4:ce:e1:6a:55:1f:8b:17:00:e6:69:a3:27:
                    e6:08:25:69:3c:12:9d:8d:05:2c:d6:2e:a2:31:de:
                    b4:52:50:d6:20:49:de:71:a0:f9:ad:20:40:12:f1:
                    dd:25:eb:d5:e6:b8:36:f4:d6:8f:7f:ca:43:dc:d7:
                    10:5b:e6:3f:51:8a:85:b3:f3:ff:f6:03:2d:cb:23:
                    4f:9c:ad:18:e7:93:05:8c:ac:52:9a:f7:4c:e9:99:
                    7a:be:6e:7e:4d:0a:e3:c6:1c:a9:93:fa:3a:a5:91:
                    5d:1c:bd:66:eb:cc:60:dc:86:74:ca:cf:f8:92:1c:
                    98:7d:57:fa:61:47:9e:ab:80:b7:e4:48:80:2a:92:
                    c5:1b
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:TRUE, pathlen:1
    Signature Algorithm: sha1WithRSAEncryption
    Signature Value:
        8b:d4:a1:b1:cf:5d:cd:7b:6c:48:4a:41:1f:53:2f:18:2d:32:
        45:ff:9e:ab:d3:73:3e:1f:22:d7:ea:fa:01:e6:73:03:0f:2b:
        c6:25:bb:a5:ee:c5:f5:45:cb:24:12:2a:ad:c2:5d:05:f4:7a:
        f5:c2:9b:10:16:5a:d1:0a:73:c5:16:39:a0:10:ca:d1:68:85:
        9e:fb:9e:26:83:8e:58:f3:77:a0:4e:e5:db:97:be:2d:00:5f:
        f5:94:db:b1:9d:65:6b:fd:f0:d1:04:51:df:cc:92:a6:99:2d:
        71:f5:4d:d5:23:fe:33:1c:a9:b4:ab:c5:bf:1a:b8:d1:80:ef:
        89:c9:e2:1f:9c:4c:48:3b:a2:fa:02:0a:dc:84:01:8a:87:02:
        fb:59:ee:a7:4c:04:7d:74:99:87:6a:25:44:ad:16:aa:ec:4e:
        35:1b:7c:7b:84:c9:b1:3f:e1:82:70:e5:0d:e7:d9:6d:fa:95:
        b6:c5:e4:1e:e8:11:9b:d8:b2:f3:a4:fd:13:f3:83:4f:f7:07:
        14:20:bb:22:a5:a6:8f:d6:b5:db:a9:74:78:e2:93:0d:e5:23:
        2f:05:17:e0:b2:97:67:34:4d:0f:9c:76:43:7b:a6:21:4a:56:
        05:f6:2a:7c:f2:7f:12:94:82:26:29:07:f0:0b:6c:6c:79:14:
        b0:74:d5:6c
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is the same certificate as viewed in Apple’s Keychain app.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/chromecast-device-authentication/ica-certificate.png&quot; alt=&quot;ICA Certificate&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Here we can see the Common Name associated with the certificate (Eureka Gen1 ICA) - Eureka being the original codename for Chromecast.&lt;/p&gt;

&lt;h3 id=&quot;challenge-request&quot;&gt;Challenge Request&lt;/h3&gt;

&lt;p&gt;This gives us the pieces we need to under the Challenge Request.&lt;/p&gt;

&lt;p&gt;When a client first connects to a receiver, it may issue a Challenge Request. This is a message that instructs the receiver to sign a hash of its Peer Certificate using the &lt;em&gt;Device Private Key&lt;/em&gt;, and to respond with an x509 certificate chain that can be used to verify that this signature is valid.&lt;/p&gt;

&lt;p&gt;Here is the overall process:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The client re-hashes the &lt;em&gt;Peer Certificate&lt;/em&gt;, then uses the &lt;em&gt;Device Certificate&lt;/em&gt; to verify that the signature was generated by that particular receiver. i.e. the signature is verified using the device’s public key contained in the &lt;em&gt;Device Certificate&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;The client then verifies that the &lt;em&gt;Device Certificate&lt;/em&gt; was signed by an ICA corresponding to the particular model of Chromecast device. In the challenge response, the receiver is expected to provide an ICA Certificate for this purpose.&lt;/li&gt;
  &lt;li&gt;Finally, the client verifies that the ICA Certificate was signed by a trusted Root CA. For this, it will use a Root CA certificate that is included with Google Chrome or Android OS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;aside-chromecast-outage&quot;&gt;Aside: Chromecast Outage&lt;/h2&gt;

&lt;p&gt;As an aside, we now have everything we need to know to understand the recent Chromecast outage.&lt;/p&gt;

&lt;p&gt;If we simply take a look at the ICA Certificate used for second-gen Chromecast devices - specifically the &lt;strong&gt;Validity&lt;/strong&gt; period - we’ll see that the certificate expired on 9th March 2025:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 36 (0x24)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, ST=California, L=Mountain View, O=Google Inc, OU=Cast, CN=Cast Root CA
        Validity
            Not Before: Mar 12 16:44:39 2015 GMT
            Not After : Mar  9 16:44:39 2025 GMT
        Subject: C=US, ST=California, L=Mountain View, O=Google Inc, OU=Cast, CN=Chromecast ICA 3
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:d1:de:fb:ad:8b:43:07:28:ae:56:2d:f2:73:2a:
                    1f:63:43:76:6d:8d:b8:d1:d4:90:29:1b:91:68:4a:
                    55:41:a0:d5:61:b4:ec:dd:ae:e1:fa:a7:b6:38:c4:
                    de:19:e1:33:4d:9a:29:f1:48:e2:6b:a7:2c:21:14:
                    22:3f:87:81:f3:71:2c:e6:43:1c:b8:d4:ec:cf:67:
                    2f:b2:a2:75:8b:10:bd:f9:e7:c9:5c:de:05:a9:b4:
                    86:b7:68:7d:a7:76:85:e2:65:b8:76:51:4f:b9:60:
                    5d:7e:2b:64:48:12:66:d9:a7:bb:7c:d7:48:88:8a:

                    &amp;lt;// SNIP //&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The reason it was so difficult for Google to respond to this issue is that second-gen Chromecasts were hard-coded to use this particular certificate.&lt;/p&gt;

&lt;h2 id=&quot;problem-statement&quot;&gt;Problem Statement&lt;/h2&gt;

&lt;p&gt;At this stage, we can begin to flesh out the problems that we need to solve:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;We need to gain access to the Device Private Key from a genuine Chromecast device. Failing that, we need to coax a device to sign a Peer Certificate for us.&lt;/li&gt;
  &lt;li&gt;We need to find the Device Certificate that corresponds to the Device Private Key.&lt;/li&gt;
  &lt;li&gt;We need to find the ICA Certificate that was used to sign the Device Certificate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given the statements above - in particular the restricted access to the Device Private Key - this &lt;em&gt;may&lt;/em&gt; appear impossible.&lt;/p&gt;

&lt;h3 id=&quot;prior-work&quot;&gt;Prior Work&lt;/h3&gt;

&lt;p&gt;While I was initially investigating the Google Cast protocol, I discovered other Chromecast Receiver implementations that did not appear to be officially licensed from Google. Somehow they have solved this problem, so I knew it would be possible to bypass or spoof the Device Authentication mechanism.&lt;/p&gt;

&lt;p&gt;My investigation focused on two products, AirServer and Reflector. Using Wireshark and an executable reverse engineering tool called &lt;a href=&quot;https://www.hopperapp.com/&quot;&gt;Hopper&lt;/a&gt; for macOS, I dug into how both products work internally. Ultimately, it was Reflector that allowed me figure out how to implement a viable Device Authentication mechanism.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/chromecast-device-authentication/hopper.png&quot; alt=&quot;Hopper&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;certificate-manifest&quot;&gt;Certificate Manifest&lt;/h3&gt;

&lt;p&gt;The Chromecast receiver functionality in Reflector depends upon something that I’ve been calling a &lt;strong&gt;Certificate Manifest&lt;/strong&gt;. This is a misleading name, as it contains more than just certificates, but I haven’t been able to come up with anything better.&lt;/p&gt;

&lt;p&gt;The Certificate Manifest provides all of the information contained in the blue boxes in the Device Authentication diagram shown earlier:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Peer Private Key&lt;/strong&gt; and &lt;strong&gt;Peer Certificate&lt;/strong&gt; - Used to secure the initial TLS connection. Generated periodically by a Chromecast Receiver.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ICA Certificate&lt;/strong&gt; - This represents the Intermediate Certificate Authority (ICA) that issues device certificates. This is used for traditional PKI certificate chain verification. Each generation / product line of Chromecast has its own ICA.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Device Certificate&lt;/strong&gt; - This is a per-device certificate, corresponding to the private key embedded in the device’s secure enclave.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Peer Certificate Signature&lt;/strong&gt; - Used to ensure that the device is a genuine or licensed Chromecast Receiver. This is signed using the private key embedded within any genuine Chromecast device.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All together, that is five separate items that a receiver needs to pass Device Authentication.&lt;/p&gt;

&lt;h3 id=&quot;tls-server&quot;&gt;TLS Server&lt;/h3&gt;

&lt;p&gt;The Peer Private Key and Peer Certificate are the most basic requirements here. These are needed to start a TLS server that can accept connections on port 8009.&lt;/p&gt;

&lt;p&gt;Once we are able to accept connections on port 8009, the next problem we need to solve is the Device Authentication Challenge. This is where a Chromecast sender expects us to prove that our receiver is a genuine, or licensed, Chromecast device. To do this, we need to figure out how to sign the Peer Certificate using the Private Key that is embedded in a real Chromecast device.&lt;/p&gt;

&lt;p&gt;And in order to do this, we’ll need to gain root access to the Chromecast…&lt;/p&gt;

&lt;h2 id=&quot;root-access&quot;&gt;Root Access&lt;/h2&gt;

&lt;p&gt;A little research was enough to discover that there is a way to gain root access to a Chromecast. The catch is that this was only possible with a first-generation Chromecast, and even then, only one that had not been updated to the latest firmware (i.e. never connected to the internet).&lt;/p&gt;

&lt;p&gt;Amazingly, I was able to find not one, but two mint condition first-gen Chromecast units online, and quickly ordered these for safe keeping:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/chromecast-device-authentication/first-generation-chromecasts.jpg&quot; alt=&quot;First-generation Chromecasts&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I apologise for hoarding these. For this project, it was crucial that I had a backup device, in case the first was bricked while doing research. Of course, I have been very careful to avoid connecting either device to the internet with the stock firmware. Even a few minutes of internet connectivity would be enough for the device to download a firmware update that closes the vulnerability allowing root access.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/chromecast-device-authentication/xray-vision.png&quot; alt=&quot;X-ray Vision&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The first generation Chromecast was not just physically vulnerable, but at the time, the Chromecast firmware was also vulnerable to an attack in which the Chromecast could be booted using an external USB device.&lt;/p&gt;

&lt;h3 id=&quot;eureka-rom-firmware&quot;&gt;Eureka-ROM Firmware&lt;/h3&gt;

&lt;p&gt;Eureka-ROM is a third-party Chromecast ROM, maintained by Team Eureka. It includes features such as ADB, SSH and Telnet access, and DNS customization. Other parts of the OS remain unchanged. The ROM is playfully named after the original codename for the Chromecast project: Eureka.&lt;/p&gt;

&lt;p&gt;Eureka-ROM also includes and a web-based control panel, which can be useful for debugging rooted devices:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/chromecast-device-authentication/eurekarom-homepage.png&quot; alt=&quot;Eureka-ROM Homepage&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Crucially, we’ll be relying on ADB and SSH access in order to run our own binaries on a rooted Chromecast.&lt;/p&gt;

&lt;h3 id=&quot;flashing-process&quot;&gt;Flashing Process&lt;/h3&gt;

&lt;p&gt;The actual process of rooting the device is interesting. The process requires a &lt;a href=&quot;https://www.pjrc.com/teensy/&quot;&gt;Teensy++&lt;/a&gt;, a simple USB-based micro-controller development board that we will connect to the Chromecast via USB. The Teensy++ allows us to boot the Chromecast into a mode that allows a new firmware image to be written to the device.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/chromecast-device-authentication/teensy.png&quot; alt=&quot;Teensy++&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The first thing we need to do is program the Teensy++ with firmware (called HubCap) that allows it to communicate with the Chromecast. This is done by connecting the Teensy++ to a PC and using &lt;a href=&quot;https://www.pjrc.com/teensy/loader.html&quot;&gt;Teensy Loader&lt;/a&gt; to program the micro-controller. We also need to prepare a USB thumb drive with the firmware (Eureka-ROM) that we intend to install on the Chromecast. This is not a regularly formatted USB thumb drive. Instead, we need to use a disk imaging tool to write the firmware image to the drive.&lt;/p&gt;

&lt;p&gt;With all this ready, we can connect the Teensy++ to the Chromecast, using an externally-powered USB OTG cable:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/chromecast-device-authentication/teensy-setup.jpg&quot; alt=&quot;Teensy++ Setup&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Once the Chromecast has been booted into a mode that allows us to upload new firmware, we replace the Teensy++ with the USB thumb drive containing our custom firmware. Updating the firmware on the Chromecast takes up to 10 minutes, so when I was testing this, I would just set a timer and go do something else in the meantime. If everything has worked as intended, when we next boot the Chromecast, we’ll see this lovely splash screen:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/chromecast-device-authentication/flashing-in-progress.png&quot; alt=&quot;Flashing in progress...&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;gotchas&quot;&gt;Gotchas&lt;/h3&gt;

&lt;p&gt;It’s important to point out that, at time of writing, these steps will render your Chromecast useless, unless you go through the steps to re-flash it with official firmware.&lt;/p&gt;

&lt;p&gt;I found that versions &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;17977.001&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;44433.001&lt;/code&gt; of Eureka-ROM are both too old to be compatible with modern versions of Google Chrome. My attempts to upgrade to later versions were entirely unsuccessful. As such, it’s best to think of any Chromecast used for signature generation as a donor device.&lt;/p&gt;

&lt;p&gt;For reasons explained below, I settled for version &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;17977.001&lt;/code&gt; of the Eureka-ROM firmware.&lt;/p&gt;

&lt;h2 id=&quot;signature-generation&quot;&gt;Signature Generation&lt;/h2&gt;

&lt;p&gt;We’re almost there!&lt;/p&gt;

&lt;p&gt;As mentioned above, a Chromecast Receiver is expected to generate a new TLS private key and self-signed certificate at least once every 48 hours. I generally refer to these as the &lt;strong&gt;Peer Private Key&lt;/strong&gt; and &lt;strong&gt;Peer Certificate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a Device Authentication Challenge is received, we need to calculate the SHA1 hash of the Peer Certificate (this will be X bytes long) and sign this using Device Private Key.&lt;/p&gt;

&lt;h3 id=&quot;chromecast-tools&quot;&gt;Chromecast Tools&lt;/h3&gt;

&lt;p&gt;When first researching this project, I found a GitHub repo called &lt;a href=&quot;https://github.com/EiNSTeiN-/chromecast-widevine-tools&quot;&gt;chromecast-widevine-tools&lt;/a&gt;. This contains source code for tools that can be run on a rooted Chromecast in order to use the cryptographic functionality of the device, including signing a hash using the private key. I have no idea how the original author came up with any of this, but remarkably, it works.&lt;/p&gt;

&lt;p&gt;Compiling these tools was a little tricky, because I had to obtain an appropriate toolchain, and set up the necessary development packages on a Linux system. At the conclusion of this project, I created a fork of this repo, &lt;a href=&quot;https://github.com/tristanpenman/chromecast-tools&quot;&gt;chromecast-tools&lt;/a&gt;, which contains just the code needed to sign data using the private key. This makes it sufficient for Device Authentication.&lt;/p&gt;

&lt;p&gt;My fork also includes a Dockerfile to make compiling a bit easier:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git clone git@github.com:tristanpenman/chromecast-tools.git
cd chromecast-tools
docker build . -t chromecast-tools
docker run -v `pwd`:/workspace --rm -it chromecast-tools /bin/bash
make
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This produces a binary called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtv-ca-sign&lt;/code&gt;, which can be run on the Chromecast device.&lt;/p&gt;

&lt;h3 id=&quot;aside-toolchain-compatibility&quot;&gt;Aside: Toolchain Compatibility&lt;/h3&gt;

&lt;p&gt;At this point, it’s worth mentioning some issues that I had with toolchain compatibility.&lt;/p&gt;

&lt;p&gt;In the &lt;a href=&quot;#gotchas&quot;&gt;Gotchas&lt;/a&gt; section above, I mentioned that I tried versions &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;17977.001&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;44433.001&lt;/code&gt; of the Eureka-ROM firmware and ultimately settled for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;17977.001&lt;/code&gt;. The reason for this is toolchain compatibility.&lt;/p&gt;

&lt;p&gt;A toolchain contains all the libraries, headers, and other supporting files required to compile programs for a specific architecture and operating system. When cross-compiling (as we are here) it is crucial that the toolchain is compatible with the libraries and OS installed on the target device.&lt;/p&gt;

&lt;p&gt;What I found is that between versions &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;17977.001&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;44433.001&lt;/code&gt;, some kind of breaking changes had occurred. The result is that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtv-ca-sign&lt;/code&gt; binary would be compiled successfully, but running it on the device would fail with an error like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gtv-ca-sign: not found
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I chose not to investigate this further, and simply revert to the working firmware.&lt;/p&gt;

&lt;h3 id=&quot;mining-certificates&quot;&gt;Mining Certificates&lt;/h3&gt;

&lt;p&gt;This is the final piece of the puzzle.&lt;/p&gt;

&lt;p&gt;The first step in successfully passing Device Authentication is to start a TLS server using a Peer Private Key and self-signed Peer Certificate that have been generated within the last 24 hours, and are valid for no longer than 48 hours. A Chromecast Sender will immediately close the connection if these conditions are not met!&lt;/p&gt;

&lt;p&gt;We also need to generate a cryptographic &lt;em&gt;hash&lt;/em&gt; of the Peer Certificate, which we will sign using the Private Key belonging to our rooted Chromecast.&lt;/p&gt;

&lt;p&gt;I wrote a little Python script to generate an appropriate Peer Private Key and Peer Certificate. This script also calculates the SHA256 hash of the Peer Certificate:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OpenSSL&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crypto&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;datetime&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;datetime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timedelta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;timezone&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashlib&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sys&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;today&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;today&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get_iso_timestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;day_offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;timestamp_string&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;datetime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;combine&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;today&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;timedelta&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;days&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;day_offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                                      &lt;span class=&quot;nf&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                                      &lt;span class=&quot;n&quot;&gt;tzinfo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timezone&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;utc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;strftime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;%Y%m%d%H%M%S&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timestamp_string&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Z&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Generate a key
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;k&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;PKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;TYPE_RSA&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2048&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Start building our certificate
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cert&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;X509&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get_subject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CN&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;FakeChromecast&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;set_serial_number&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;12345678&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;set_pubkey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;set_issuer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get_subject&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# self-signed
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Set the validity period
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;set_notBefore&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get_iso_timestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 24 hours ago
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;set_notAfter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;get_iso_timestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 24 hours from now
&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Sign the cert
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sign&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;sha1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Write the cert to a file
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;peer.crt&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;wb&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dump_certificate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FILETYPE_PEM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Write the key to a file
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;peer.key&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;wb&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dump_privatekey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FILETYPE_PEM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;k&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Print out a SHA256 hash of the Peer Certificate
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;der&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dump_certificate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;crypto&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FILETYPE_ASN1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;sha&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashlib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sha256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;der&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hexdigest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sha&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That last bit (printing the hash) is important, because we need to sign the SHA256 hash of the Peer Certificate using the private key embedded in our rooted Chromecast. We do this by passing the hash to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gtv-ca-sign&lt;/code&gt; (replacing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;HASH&amp;gt;&lt;/code&gt; with the hash that was printed by the script):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;adb shell /tmp/gtv-ca-sign --hash &amp;lt;HASH&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will produce the signature that we need when responding to a Device Authentication challenge!&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;When I first started this project (several years ago) it was intended to be a small proof-of-concept. In the years since, it became an interesting personal project that challenged me to learn Go. While my implementation could only ever be considered a toy, it helped me to internalise important concepts relating to PKI and the security of physical devices.&lt;/p&gt;

&lt;p&gt;This has become relevant once again, courtesy of the recent Chromecast outage. Crucially, doing what we can to spread knowledge about how these security mechanisms work will help ensure that future devices are designed &lt;em&gt;without&lt;/em&gt; such vulnerabilities.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;p&gt;Open source projects relevant to this post:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/tristanpenman/go-cast&quot;&gt;Go-Cast&lt;/a&gt; - My own Chromecast Receiver implementation, that I wrote while learning Go. It implements Device Authentication, basic app discovery, and rudamentary screen mirroring using VP8. At time of writing, the RTP implementation is still very buggy…&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/tristanpenman/chromecast-tools&quot;&gt;Chromecast Tools&lt;/a&gt; - My fork of &lt;a href=&quot;https://github.com/EiNSTeiN-/chromecast-widevine-tools&quot;&gt;chromecast-widevine-tools&lt;/a&gt; that contains just the code needed for generating certificates and signing Peer Certificates.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://chromium.googlesource.com/openscreen/&quot;&gt;Open Screen Library&lt;/a&gt; - Google’s Open Screen Library, which includes implementations for Google Cast Senders and Receivers. This is more complete than my Go implementation, and would be an excellent reference for those wanting to implement Google Cast themselves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Related projects found during the proof-of-concept phase:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/thibauts/node-castv2&quot;&gt;node-castv2&lt;/a&gt; - An implementation of the Google Cast protocol in JavaScript&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/thibauts/node-castv2-client&quot;&gt;node-castv2-client&lt;/a&gt; - A corresponding client implementation written in JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Interesting reading:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/Chromecast/comments/1j7lhrs/the_chromecast_2s_device_authentication/&quot;&gt;The Chromecast 2’s device authentication certificate has expired&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arstechnica.com/gadgets/2025/03/google-has-a-fix-for-your-broken-chromecast-v2-unless-you-factory-reset/&quot;&gt;Google has a fix for your broken Chromecast V2 unless you factory reset&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://archive.org/details/paper_202302&quot;&gt;Forensic analysis of Chromecast and Miracast devices&lt;/a&gt; - Peter van Bolhuis, Cedric Van Bockhaven&lt;/li&gt;
&lt;/ul&gt;
</description>
					<pubDate>Sat, 22 Mar 2025 00:00:00 +0000</pubDate>
					<link>https://tristanpenman.com/blog/posts/2025/03/22/chromecast-device-authentication/</link>
					<guid isPermaLink="true">https://tristanpenman.com/blog/posts/2025/03/22/chromecast-device-authentication/</guid>
				</item>
			
		
			
				<item>
					<title>Roll Your Own Ruby Type Checking: Part 3 </title>
					
					<description>&lt;p&gt;In parts &lt;a href=&quot;https://tristanpenman.com/blog/posts/2022/12/26/roll-your-own-ruby-type-checking-part-1/&quot;&gt;one&lt;/a&gt; and &lt;a href=&quot;https://tristanpenman.com/blog/posts/2023/05/13/roll-your-own-ruby-type-checking-part-2/&quot;&gt;two&lt;/a&gt; of this series, we built a basic runtime type checker for Ruby. The design of our type checker is heavily inspired by Sorbet. So much so that we run up against some of the same limitations as Sorbet. This post focuses on one of those limitations - the inability to perform type checks on parameters with default values. To better understand this, we’ll look at several examples in both Ruby and Python.&lt;/p&gt;

&lt;h2 id=&quot;ruby-parameter-types&quot;&gt;Ruby Parameter Types&lt;/h2&gt;

&lt;p&gt;Recall from part two, the six parameter types that our type checker is currently designed to support. We used &lt;em&gt;reflection&lt;/em&gt; to derive this list:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;req&lt;/code&gt; - required positional&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opt&lt;/code&gt; - optional positional (has default value)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rest&lt;/code&gt; - additional positional arguments&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keyreq&lt;/code&gt; - required keyword parameter&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;key&lt;/code&gt; - optional keyword parameter&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keyrest&lt;/code&gt; - additional keyword parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two that concern us in this post are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opt&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;key&lt;/code&gt;, as both of these represent parameters that have a default value.&lt;/p&gt;

&lt;p&gt;So what’s the actual issue here? Well, it turns out that neither Sorbet, nor our type checker, are able to check the types of those default values!&lt;/p&gt;

&lt;h2 id=&quot;breaking-sorbet&quot;&gt;Breaking Sorbet&lt;/h2&gt;

&lt;p&gt;I originally wanted to title this post &lt;em&gt;Breaking Sorbet&lt;/em&gt; and to publish it independently of this series. But it wouldn’t be fair to call out Sorbet for this, as it is a limitation that stems from decisions made during the design of Ruby’s optional parameter types. In fact, I only discovered this issue while testing my own type checker under different scenarios.&lt;/p&gt;

&lt;p&gt;To see how this affects type checking in Sorbet, check out the following example:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;sorbet-runtime&apos;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Main&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;extend&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Sig&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;sig&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;kw: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;void&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kw&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;kw: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kw&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;kw.is_a?(String): &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here we’re using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Time.now&lt;/code&gt; as the default value for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; method, and this will be evaluated every time the method is called.&lt;/p&gt;

&lt;p&gt;First we’ll try calling the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; method with an invalid argument:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;begin&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;123&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;rescue&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;StandardError&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Error: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This will fail as expected:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Error: Parameter &apos;kw&apos;: Expected type String, got type Integer with value 123
Caller: ./part-3.rb:36
Definition: ./part-3.rb:29
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If we call it without an argument, it will use the default value (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Time.now&lt;/code&gt;) that we’ve defined:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kp&quot;&gt;loop&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Somehow, this works:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kw: 2023-05-13 09:49:07 +1000
kw.is_a?(String): false
kw: 2023-05-13 09:49:08 +1000
kw.is_a?(String): false
kw: 2023-05-13 09:49:09 +1000
kw.is_a?(String): false
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can see that Sorbet’s type check is satisfied, even though we can clearly see that the value is not a Ruby &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;String&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;but-why&quot;&gt;But why?&lt;/h2&gt;

&lt;p&gt;So why is the type check succeeding here?&lt;/p&gt;

&lt;p&gt;Recall that our type checker works by wrapping a method. The wrapper knows how to check the types of its arguments, before passing those as arguments to the original method. This works fine when there are arguments to check. But in the case of default values, there arguments are absent. And this means that the Ruby interpreter evaluates the expression for the default value as the original method is called. The only way we would be able to inspect that value would be to inject code &lt;em&gt;into&lt;/em&gt; the method, which is far beyond the scope of our type checker.&lt;/p&gt;

&lt;p&gt;In a nutshell, default values are evaluated at the time that a method is invoked.&lt;/p&gt;

&lt;h3 id=&quot;the-python-way&quot;&gt;The Python Way&lt;/h3&gt;

&lt;p&gt;Interestingly, this is not necessarily how other languages handle default values… In Python, for example, default values are evaluated at the time that a method is defined. We can run a little experiment (in Python of course) to see how this behaves:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;arr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()):&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nf&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;nf&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You might expect this to produce the following:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[1, 2, 3, 4]
[8, 9, 10, 4]
[1, 2, 3, 4]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But instead we get this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[1, 2, 3, 4]
[8, 9, 10, 4]
[1, 2, 3, 4, 4]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This shows us that:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;The array has been passed to the method body by-reference.&lt;/li&gt;
  &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arr()&lt;/code&gt; method is only called once, at the time that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; method was defined.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This can lead to bugs that are very confusing and hard to track down, and it is notorious for tripping up novice Python programmers!&lt;/p&gt;

&lt;h2 id=&quot;solutions&quot;&gt;Solutions?&lt;/h2&gt;

&lt;p&gt;Is there anything we can do about this? Well, maybe…&lt;/p&gt;

&lt;p&gt;It may be possible with some significant changes to the Ruby interpreter, such as the addition of more powerful reflection and metaprogramming functionality. This would allow purely Ruby-based type checkers greater access to the method invocation process. This could have applications for performance monitoring and other low-level problems.&lt;/p&gt;

&lt;p&gt;Another approach that I would like to explore is the use of runtime code generation - effectively reading the bytecode that is generated for a method, and to modify it such that the type checking occurs inside the method body. However this lies far outside the scope of this series, so don’t expect to see that for while!&lt;/p&gt;
</description>
					<pubDate>Sat, 20 May 2023 00:00:00 +0000</pubDate>
					<link>https://tristanpenman.com/blog/posts/2023/05/20/roll-your-own-ruby-type-checking-part-3/</link>
					<guid isPermaLink="true">https://tristanpenman.com/blog/posts/2023/05/20/roll-your-own-ruby-type-checking-part-3/</guid>
				</item>
			
		
			
				<item>
					<title>Roll Your Own Ruby Type Checking: Part 2 </title>
					
					<description>&lt;p&gt;In my &lt;a href=&quot;https://tristanpenman.com/blog/posts/2022/12/26/roll-your-own-ruby-type-checking-part-1/&quot;&gt;last post&lt;/a&gt; on Ruby Type Checking, we looked at how metaprogramming could be used to implement an annotation-style syntax in Ruby. We then leveraged these techniques to build a simple runtime type checker, heaviliy inspired by &lt;a href=&quot;https://sorbet.org&quot;&gt;Sorbet&lt;/a&gt;. Unfortunately, our type checker is woefully incomplete. In this post, we take another step towards making it useful.&lt;/p&gt;

&lt;p&gt;The code for this post can be found in my &lt;a href=&quot;https://github.com/tristanpenman/ruby-type-checking&quot;&gt;ruby-type-checking&lt;/a&gt; repo on GitHub.&lt;/p&gt;

&lt;h2 id=&quot;recap&quot;&gt;Recap&lt;/h2&gt;

&lt;p&gt;The examples in the first post were based on a toy class called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Repeater&lt;/code&gt;. This class includes one method, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;repeat&lt;/code&gt;, whose sole purpose is to repeat a string (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;str&lt;/code&gt;) a given number times (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt;), with each copy of the string separated by another string (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;separator&lt;/code&gt;). We made this more interesting by deciding that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;str&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; should be positional parameters, while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;separator&lt;/code&gt; would be a keyword parameter.&lt;/p&gt;

&lt;p&gt;By the end of the first post, our final type checked example looked like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Repeater4&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;extend&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Types&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;typedef&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Numeric&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;limitations&quot;&gt;Limitations&lt;/h2&gt;

&lt;p&gt;Our type checker was complete enough to support the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Repeater&lt;/code&gt; example. However, it is still limited in various ways. In particular, it is unable to represent the full range of parameter types supported by Ruby. At a high level, these include:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Rest Parameters&lt;/strong&gt; - Rest Parameters allow an arbitrary number of arguments to be passed to a method. e.g. the method &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;def my_func(tag, *args)&lt;/code&gt; requires one argument, followed by zero or more additional arguments. Our type checker does not support this.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Optional Parameters&lt;/strong&gt; - These are positional parameters or keyword parameters with default values.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Block Parameters&lt;/strong&gt; - Methods can accept a block as an argument, and can call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yield&lt;/code&gt; to pass control to that block. This is commonly used for methods like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;each&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;map&lt;/code&gt; on arrays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our type system is also quite simplistic, and it isn’t difficult to imagine a couple of ways in which it could be improved:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Compound Types&lt;/strong&gt; - It would be nice to be able to represent compound types. These are types that come from combining other types, e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OneOf(String, Numeric)&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Array and Hash Types&lt;/strong&gt; - It is also common to pass arrays and hashes as method arguments, and we would like some way to represent this, e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Array(String)&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hash(key1: String, key2: Numeric)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Addressing all of these limitation would be too much for one post, so we’ll focus on &lt;em&gt;Rest Parameters&lt;/em&gt; and &lt;em&gt;Optional Parameters&lt;/em&gt; for now. This alone will bring us much closer to having a useful type checker.&lt;/p&gt;

&lt;p&gt;Adding support for Blocks Parameters, or Compound, Array and Hash Types will be the topic of a future post.&lt;/p&gt;

&lt;h2 id=&quot;rest-parameters&quot;&gt;Rest Parameters&lt;/h2&gt;

&lt;p&gt;Lets begin with Rest Parameters. If we revisit the code from our first type checker, we can see that we validate positional arguments by iterating over an array of expected argument types:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;check_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kwarg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# check positional arguments&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each_with_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid type for arg &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;; expected: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;c1&quot;&gt;# check keyword arguments&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;kwarg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid type for kwarg &apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`; expected &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwarg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Keyword arguments are validated using a similar approach, but in that case we’re iterating over the keys in a Hash.&lt;/p&gt;

&lt;p&gt;This implementation assumes that a method will always accept a fixed number of positional arguments, followed by zero or more keyword arguments, like our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;repeat&lt;/code&gt; method:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Unfortunately, this isn’t the complete picture. We may have a method like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;send_messages&lt;/code&gt; below, that accepts a single positional argument, followed by a variable number of positional arguments:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;send_messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recipient_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;send_message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recipient_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The asterisk (splat operator) means that, within our method body, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;messages&lt;/code&gt; will actually refer to an array containing all additional positional arguments.&lt;/p&gt;

&lt;h3 id=&quot;how-does-sorbet-do-it&quot;&gt;How does Sorbet do it?&lt;/h3&gt;

&lt;p&gt;Clearly we’ll need to take a different approach, that allows us to annotate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*messages&lt;/code&gt; with a type, even though it may represent zero or more positional arguments.&lt;/p&gt;

&lt;p&gt;The way Sorbet approaches this is to assign types to &lt;em&gt;parameter names&lt;/em&gt;, even those that are positional. Therefore, the type for a variable-length argument list such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*messages&lt;/code&gt; is a type that must be satisfied by all elements in the array. The Sorbet solution looks like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;sig&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Integer describes a required positional argument&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;recipient_id: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Integer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# String describes every single value of the `messages` array,&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# since `messages` uses the splat operator&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;messages: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;void&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;send_messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recipient_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;send_message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recipient&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We’ll need to make some changes to our type checker to support this.&lt;/p&gt;

&lt;h2 id=&quot;optional-parameters&quot;&gt;Optional Parameters&lt;/h2&gt;

&lt;p&gt;The other case we are going to address is optional parameters. Optional parameters are those that have default values, and may be omitted. There are two kinds that we need to cater for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Optional &lt;em&gt;positional&lt;/em&gt; parameters&lt;/strong&gt; - These must come after required positional parameters, but before additional/rest parameters and keyword parameters.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Optional &lt;em&gt;keyword&lt;/em&gt; parameters&lt;/strong&gt; - These must come after &lt;em&gt;positional&lt;/em&gt; parameters, but can appear anywhere in a sequence of keyword parameters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes things more complicated. At this stage, it’s worth digging into the different kinds of method parameters that Ruby supports. One way we can do this, is to use reflection to inspect the parameters for a method.&lt;/p&gt;

&lt;h3 id=&quot;parameter-types&quot;&gt;Parameter Types&lt;/h3&gt;

&lt;p&gt;Here is a simple example, which includes six different parameter types (blocks are excluded). This method uses reflection to print out the type for each of its parameters:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;e: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__method__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;When we call it:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;test1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;d: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;e: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;f: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;g: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;we’ll get a list of parameters and what kind they are:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[:req, :a]
[:opt, :b]
[:rest, :c]
[:keyreq, :d]
[:key, :e]
[:keyrest, :f]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This gives us six kinds:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;req&lt;/code&gt; - required positional&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;opt&lt;/code&gt; - optional positional (has default value)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rest&lt;/code&gt; - additional positional arguments&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keyreq&lt;/code&gt; - required keyword parameter&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;key&lt;/code&gt; - optional keyword parameter&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keyrest&lt;/code&gt; - additional keyword parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The weird naming here is a reflection of Ruby’s history, with different keyword parameter types being introduced at different times.&lt;/p&gt;

&lt;h3 id=&quot;binding&quot;&gt;Binding&lt;/h3&gt;

&lt;p&gt;Now we can go a step further, and use reflection to see how arguments are bound to parameter names:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;e: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__method__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# parameter name&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# argument&lt;/span&gt;
      &lt;span class=&quot;nb&quot;&gt;binding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;local_variable_get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_s&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Let’s see what happens when we omit arguments for any optional parameters:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;test2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;d: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;f: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;g: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Running this, we get:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[&quot;a&quot;, 1]
[&quot;b&quot;, 2]
[&quot;c&quot;, []]
[&quot;d&quot;, 5]
[&quot;e&quot;, 6]
[&quot;f&quot;, {:f=&amp;gt;7, :g=&amp;gt;8}]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can see that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e&lt;/code&gt; have been bound to their default values. We can also see that the rest parameter &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c&lt;/code&gt; is bound to an empty array, as there were no extra positional arguments.&lt;/p&gt;

&lt;h2 id=&quot;new-type-checker&quot;&gt;New Type Checker&lt;/h2&gt;

&lt;p&gt;We can put all of this together to write a new version of our type checker. Here is the core of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Types&lt;/code&gt; module:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Types&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# we accept just a hash of key-values, mapping parameters to argument types&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@arg_types&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@ret_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;typedef&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;method_added&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# short-circuit, to avoid infinite loop&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@arg_types&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@ret_type&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# reset hook context, but store current values&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@ret_type&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@ret_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# capture the original method&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;meth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;instance_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;meth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;parameters&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# wrap the original method with type checks&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;define_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Helpers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;check_positional_args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Helpers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;check_keyword_args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;meth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Helpers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;check_return_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;To make this more maintainable, the actual type checking code has been moved into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Helpers&lt;/code&gt; module. It’s here that we implement our new name-based type checker:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Types&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Helpers&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;check_positional_args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each_with_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;param&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:req&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:opt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:rest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;include?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;param_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

            &lt;span class=&quot;c1&quot;&gt;# Updated as long as their are positional parameter names to&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# consume. Once there aren&apos;t any more to consume, we must be&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# checking additional arguments, and can keep using whatever&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# the last type was for that.&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nil?&lt;/span&gt;

            &lt;span class=&quot;c1&quot;&gt;# Happens if there are positional arguments without a corresponding&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# type for the rest parameter&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Missing type for &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; parameter `&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
              &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nil?&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

          &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid arg at position &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;, expected &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;check_keyword_args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# TODO: not implemented yet&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;check_return_value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid return type, expected &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;keyword-parameters&quot;&gt;Keyword Parameters&lt;/h2&gt;

&lt;p&gt;Finally, we can tackle validation of keyword parameters, using our new approach. This is slightly trickier than it may sound, as we would like to detect when a method is called with additional keyword arguments that are either unexpected, or of an incorrect type.&lt;/p&gt;

&lt;p&gt;To do this, we make a copy of the keyword argument hash supplied to the method, and we remove arguments from this hash as they are determined to be correct. Anything remaining at the end must be an additional keyword argument, and should be validated using the additional keyword argument type (if specified).&lt;/p&gt;

&lt;p&gt;We can re-open our Helpers module, to implement this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Types&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Helpers&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;check_keyword_args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# don&apos;t modify the original hash&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;clone&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;param_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nil?&lt;/span&gt;

          &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:keyrest&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# only have keyrest params left, so we can break out&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;elsif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:keyreq&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid value for required kw param `&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`; expected &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
              &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;elsif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;param_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:key&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid value for optional kw param `&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`; expected &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
              &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;include?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Unexpected param type: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_type&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

          &lt;span class=&quot;c1&quot;&gt;# make sure we can detect extra kw params when they&apos;re not expected&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;param_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Unexpected extra kw params&apos;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nil?&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwarg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid value for extra kw param `&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwarg&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`; expected &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwarg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;What I hope you take away from this post is that very changes are required to make our type checker work with a wide range of Ruby parameter types. We saw how easy it is to use reflection to ticker with Ruby’s parameter binding logic, and it was by conducting these experiments that we discovered the tricks that would allow us to implement our type checker.&lt;/p&gt;

&lt;p&gt;Stay tuned for the next post in this series, in which we’ll explore some of the limits of runtime type checking in Ruby…&lt;/p&gt;
</description>
					<pubDate>Sat, 13 May 2023 00:00:00 +0000</pubDate>
					<link>https://tristanpenman.com/blog/posts/2023/05/13/roll-your-own-ruby-type-checking-part-2/</link>
					<guid isPermaLink="true">https://tristanpenman.com/blog/posts/2023/05/13/roll-your-own-ruby-type-checking-part-2/</guid>
				</item>
			
		
			
				<item>
					<title>Porting Asteroids to the Nintendo 64 </title>
					
					<description>&lt;p&gt;It was during Melbourne’s record-breaking 2020 lockdown that I rediscovered my interest in retro console development. While I had previously dabbled in homebrew for consoles from the 16-bit era, it was now the later generation consoles, such as the Nintendo 64 and SEGA Dreamcast, that caught my interest. These consoles represented the earliest days of the 3D era, each with their own quirks.&lt;/p&gt;

&lt;p&gt;This post details my experiences porting Asteroids to the Nintendo 64, and includes some pointers on how you can get started with N64 Homebrew.&lt;/p&gt;

&lt;h3 id=&quot;contents&quot;&gt;Contents&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#introduction&quot;&gt;Introduction&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#inspiration&quot;&gt;Inspiration&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#n64-architecture&quot;&gt;N64 Architecture&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#emulation&quot;&gt;Emulation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#dev-environment&quot;&gt;Dev Enviornment&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#real-hardware&quot;&gt;Real Hardware&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#n64-operating-system&quot;&gt;N64 Operating System&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#nusystem&quot;&gt;NuSystem&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#display-lists&quot;&gt;Display Lists&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#sound-tools&quot;&gt;Sound Tools&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#closing-thoughts&quot;&gt;Closing Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;It turns out that Nintendo 64 homebrew is now very accessible, with both of official and &lt;a href=&quot;https://github.com/DragonMinded/libdragon&quot;&gt;open source SDKs&lt;/a&gt; available. After discovering this, I tried porting my Asteroids clone, with the goal of having it run on real hardware. All of the code for this project can be found &lt;a href=&quot;https://github.com/tristanpenman/asteroids64&quot;&gt;on GitHub&lt;/a&gt;, and a working N64 ROM can be &lt;a href=&quot;https://tristanpenman.com/demos/asteroids/asteroids.n64&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is what it looks like in action:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/porting-asteroids-to-the-nintendo-64/asteroids64@2x.png&quot; alt=&quot;Asteroids running in cen64&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;inspiration&quot;&gt;Inspiration&lt;/h2&gt;

&lt;p&gt;I first became aware that there was an active N64 homebrew scene after accidentally discovering James Friend’s &lt;a href=&quot;https://github.com/jsdf/goose64/&quot;&gt;goose64&lt;/a&gt;, a de-make of &lt;a href=&quot;https://goose.game/&quot;&gt;Untitled Goose Game&lt;/a&gt;. His code made use of the official N64 SDK and the Windows-based development kit that many game developers would have used back in the 90s.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/porting-asteroids-to-the-nintendo-64/goose64@2x.png&quot; alt=&quot;Goose64 running in cen64&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I thought the demake project was a cool idea, and the fact that it was open source was extremely valuable in getting me started on my own projects. While exploring James’ GitHub profile, I found that he had a number of repos dedicated to N64 Homebrew. In particular, the &lt;a href=&quot;https://github.com/jsfd/n64-sdk-demo&quot;&gt;n64-sdk-demo&lt;/a&gt; repo is a good starting point for budding N64 developers.&lt;/p&gt;

&lt;p&gt;Another good starting point is the &lt;em&gt;n64&lt;/em&gt; and &lt;em&gt;N64Homebrew&lt;/em&gt; subreddits, and Discord:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/n64/&quot;&gt;/r/n64&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/N64Homebrew/&quot;&gt;/r/N64Homebrew&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/n64/comments/nbeky2/rn64_discord_servers/&quot;&gt;/r/N64 Discord Servers!&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lets move on to take a look at the Nintendo 64 architecture…&lt;/p&gt;

&lt;h2 id=&quot;n64-architecture&quot;&gt;N64 Architecture&lt;/h2&gt;

&lt;p&gt;The Nintendo 64 is built around two main processors. The main CPU is based on the &lt;a href=&quot;https://datasheets.chipdb.org/MIPS/R4300i_datasheet.pdf&quot;&gt;MIPS R4300i&lt;/a&gt;, clocked at 93.75 MHz. The CPU is complemented by the RCP (or &lt;a href=&quot;https://en.wikipedia.org/wiki/Nintendo_64_technical_specifications#Reality_coprocessor&quot;&gt;Reality Coprocessor&lt;/a&gt;) which is used for graphics and audio.&lt;/p&gt;

&lt;p&gt;The system includes two Rambus RAM chips (RDRAM), providing a total of 4.5MB of system memory (0.5MB reserved for the RCP). System memory can be expanded to 9MB (8MB usable) with the &lt;a href=&quot;https://en.wikipedia.org/wiki/Nintendo_64_accessories#Expansion_Pak&quot;&gt;N64 Expansion Pak&lt;/a&gt;. The N64 features a Unified Memory Architecture, meaning that the same memory is used by both the CPU and RCP.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/porting-asteroids-to-the-nintendo-64/n64-motherboard.png&quot; alt=&quot;N64 Motherboard&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;cpu&quot;&gt;CPU&lt;/h3&gt;

&lt;p&gt;The R4300i is a 64-bit CPU, and is considered the successor to the 32-bit R3000, the CPU that was used in the original Sony PlayStation.&lt;/p&gt;

&lt;h3 id=&quot;rcp&quot;&gt;RCP&lt;/h3&gt;

&lt;p&gt;The RCP was designed by &lt;a href=&quot;https://en.wikipedia.org/wiki/Silicon_Graphics_International&quot;&gt;SGI&lt;/a&gt;, specifically for the Nintendo 64. It consists of two main cores. One is the Reality Signal Processor, or RSP. The RSP is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Vector_processor&quot;&gt;vector processor&lt;/a&gt; based on the MIPS R4000 design and clocked at 62.5 MHz. The other is the Reality Display Processor, or RDP. While the RSP is primarily used for vector processing and mixing audio, the RDP is responsible for &lt;a href=&quot;https://en.wikipedia.org/wiki/Rasterisation&quot;&gt;rasterisation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The RCP is also used to handle joystick/controller inputs, and serves as a DMA controller for memory access. The importance of this chip is highlighted by its position on the N64 motherboard, where it occupies an even more central location than the CPU.&lt;/p&gt;

&lt;p&gt;If you want to learn more about the N64 Architecture, then I can highly recommend Rodrigo Copetti’s write-up:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.copetti.org/writings/consoles/nintendo-64&quot;&gt;Nintendo 64 Architecture - A practical analysis by Rodrigo Copetti&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;His &lt;a href=&quot;https://www.copetti.org/&quot;&gt;website&lt;/a&gt; is an absolute treasure trove of information about retro game consoles!&lt;/p&gt;

&lt;h3 id=&quot;graphics&quot;&gt;Graphics&lt;/h3&gt;

&lt;p&gt;One misconception that I held, going into this project, was that the N64 graphics APIs would closely resemble OpenGL (since both APIs were developed by SGI). In practice, there were a number of key differences that needed to be taken into account.&lt;/p&gt;

&lt;p&gt;The N64 graphics APIs are based on the concept of a Display List, which is submitted to the RSP to be processed, and then rasterised by the RDP. Display Lists are limited in size, and must carry the geometry that is to be drawn. This is quite different to the OpenGL 1.x approach, where the GL implementation can be thought of as a giant state machine, relying on direct calls to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;glLine&lt;/code&gt;, etc, or otherwise referencing vertex arrays in memory.&lt;/p&gt;

&lt;p&gt;Display Lists are discussed in more detail &lt;a href=&quot;#display-lists&quot;&gt;later in this post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;As a result, my existing graphics code would have to be re-written to work with the N64 graphics pipeline. The biggest challenge I ran into was porting my rudimentary font system. I quickly ran into resource limitations when naively copying font data into Display Lists, and had to put some effort into doing this more efficiently.&lt;/p&gt;

&lt;p&gt;Here is an example of some graphics code from my &lt;a href=&quot;https://github.com/tristanpenman/n64-homebrew.git&quot;&gt;n64-homebrew&lt;/a&gt; repo on GitHub:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// A static array of model vertex data.&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// This includes:&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//  - position (x,y,z)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//  - texture U,V coords (called S,T in the SDK)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//  - vertex color values in r,g,b,a form.&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// As this data will be read by the RCP via direct memory access,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// which is required to be 16-byte aligned, it&apos;s a good idea to&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// annotate it with the GCC attribute `__attribute__((aligned (16)))`,&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// to force it to be 16-byte aligned.&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;Vtx&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__attribute__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;aligned&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;//  x,   y,  z, flag, S, T,    r,    g,    b,    a&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// front&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;  &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;    &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;  &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;  &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;    &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;  &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;    &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;    &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;// back&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;  &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;   &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;  &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;  &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;   &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;  &lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;   &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;   &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0x00&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xff&lt;/span&gt;  &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;stage1_draw_box&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// load vertex data for the triangles&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gSPVertex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gfx_list_ptr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// depending on which graphical features, the RDP might need to&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// spend 1 or 2 cycles to render a primitive, and we need to&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// tell it which to do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gDPSetCycleType&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gfx_list_ptr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;G_CYC_1CYCLE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// use antialiasing, rendering an opaque surface&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gDPSetRenderMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gfx_list_ptr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;G_RM_AA_ZB_OPA_SURF&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;G_RM_AA_ZB_OPA_SURF2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// reset any rendering flags set when drawing the previous primitive&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gSPClearGeometryMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gfx_list_ptr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mh&quot;&gt;0xFFFFFFFF&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// enable smooth (gourad) shading and z-buffering&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gSPSetGeometryMode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gfx_list_ptr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;G_SHADE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;G_SHADING_SMOOTH&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;G_ZBUFFER&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// actually draw the triangles, using the specified vertices (front)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gSP2Triangles&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gfx_list_ptr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// back&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gSP2Triangles&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gfx_list_ptr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// right&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gSP2Triangles&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gfx_list_ptr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// left&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gSP2Triangles&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gfx_list_ptr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// Mark that we&apos;ve finished sending commands for this particular&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// primitive. This is needed to prevent race conditions inside&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// the rendering hardware in the case that subsequent commands&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// change rendering settings.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;gDPPipeSync&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gfx_list_ptr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This code is part of an example that renders &lt;a href=&quot;https://github.com/tristanpenman/n64-homebrew/blob/master/box/src/stage1.c&quot;&gt;spinning box&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/porting-asteroids-to-the-nintendo-64/spinning-box@2x.png&quot; alt=&quot;Spinning Box&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;You can see that for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gDP&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gSP&lt;/code&gt; API calls, the first argument is always a pointer into a Display List. This is post-incremented after every call, so that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gfx_list_ptr&lt;/code&gt; points to the next empty slot in the Display List.&lt;/p&gt;

&lt;h3 id=&quot;audio&quot;&gt;Audio&lt;/h3&gt;

&lt;p&gt;Audio is another case where the Nintendo 64 is a bit unusual. Whereas most home consoles would have a dedicated sound chip, or audio processing pipeline, the Nintendo 64 uses the RSP for processing audio. Similar to how Display Lists are used for graphics processing, the RSP uses Audio Command Lists for audio.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/porting-asteroids-to-the-nintendo-64/audio-architecture.png&quot; alt=&quot;Audio Architecture&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Audio can be thought of as a layered architecture, with the SDK providing ‘Players’ that are managed by the CPU. The output of these players is fed to a Synthesis driver that serialises this data in a format understood by the audio microcode running on the RSP.&lt;/p&gt;

&lt;h3 id=&quot;input&quot;&gt;Input&lt;/h3&gt;

&lt;p&gt;Cross-platform input handling required significant refactoring. Whereas the original code was event based, relying on the OS to publish input events on an event loop, the N64 expects programmers to poll for the current state of input devices.&lt;/p&gt;

&lt;p&gt;To make this consistent across the N64 and desktop versions of the code, I chose to implement an input mapping scheme. This made it possible to map multiple platform-specific inputs to a single input ID/handle. The N64 input handling code used a brute-force approach, where all relevant inputs are checked once per frame. The game code was already based on a fixed time step, so this worked well, and the overhead did not cause performance issues.&lt;/p&gt;

&lt;h3 id=&quot;storage-controller-pak&quot;&gt;Storage (Controller Pak)&lt;/h3&gt;

&lt;p&gt;There are two ways we can manage high scores. The first would be to store them in save stage on the cartridge, relying on SRAM built into the cartridge. The second is to store them using a Controller Pak, which would allow the save data to be transferred between consoles/cartridges. I opted for the Controller Pak approach.&lt;/p&gt;

&lt;p&gt;This proved to be quite challenging, as the APIs for accessing the Controller Pak require certain initialisation steps to be taken before attempting to read/write from the Controller Pak. Failing to do so leads to unpredictable behaviour, sometimes even corrupting data on the Controller Pak. I spent more time on this than I would have liked, relying on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;printf&lt;/code&gt; debugging via the EverDrive serial connection to figure out what was going wrong.&lt;/p&gt;

&lt;p&gt;As you can see from the following code snippet, just initialising the Controller Pak is quite intricate:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;open_controller_pak&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s32&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;nuContPakCodeSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;COMPANY_CODE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GAME_CODE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;nuContQueryRead&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;nuContDataReadStart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;nuContDataReadWait&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nuContStatus&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CONT_CARD_ON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;debug_printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; - nuContStatus says card is on&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;debug_printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; - nuContStatus says card is off&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;nuContPakOpen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pak_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pak_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NU_CONT_PAK_TYPE_PAK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;debug_printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; - nuContPakOpen says card is controller pak&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nuContRmbCheck&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;debug_printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; - nuContRmbCheck says card is rumble pak&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;debug_printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; - nuContPakOpen error: %d&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pak_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;debug_printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; - nuContPakRepairId attempting repair...&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nuContPakRepairId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pak_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;PFS_ERR_NEW_PACK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;debug_printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; - nuContPakRepairId failed: %d&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// try again&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;nuContPakOpen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pak_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pak_file&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NU_CONT_PAK_TYPE_PAK&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;debug_printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; - repair successful&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;debug_printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot; - repair not successful&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;haptics-rumble-pak&quot;&gt;Haptics (Rumble Pak)&lt;/h3&gt;

&lt;p&gt;While it may seem crude now, Nintendo’s Rumble Pak was yet another example of the company’s willingness to experiment with new ideas (just look at the shape of the controller itself). Force feedback is now an expected feature in all modern game controllers.&lt;/p&gt;

&lt;p&gt;The Rumble Pak requires 2 AAA batteries. Devices that take AAA batteries are typically designed to handle voltages from 0.9 to 1.5V. In practice, this could mean weaker feedback, but would not be a critical issue for the Rumble Pak. When discussing the need for AAA batteries with a friend, we guessed that this was to avoid using/straining the 3.3V that is supplied by the console.&lt;/p&gt;

&lt;p&gt;It turns out that 1) it is possible to run the Rumble Pak without batteries and 2) this would be a bad idea when more than two controllers are fitted out with Rumble Paks:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.ifixit.com/Guide/N64+Rumble+Pak+(Rumble+WITHOUT+batteries)/123571&quot;&gt;N64 Rumble Pak (Rumble WITHOUT batteries)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://retrocomputing.stackexchange.com/questions/19782/why-did-the-rumble-pak-for-the-nintendo-64-require-batteries-when-it-actually-ap&quot;&gt;Why did the Rumble Pak for the Nintendo 64 require batteries when it actually apparently doesn’t?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Driving the Rumble Pak was straightforward, using a simple API that allows game code to set the motor speed and duration for feedback events:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;rumble_start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;intensity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;u16&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;frequency&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;intensity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;u16&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;frame&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;60&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;nuContRmbModeSet&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NU_CONT_RMB_MODE_ENABLE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;nuContRmbStart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;frequency&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;frame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s interesting to note that the N64 controller was ultimately limited by a half-duplex serial interface. This means that instead of separate wires/pins for each button, there is a single data wire that carries all data. The protocol used later become known as the &lt;a href=&quot;https://n64brew.dev/wiki/Joybus_Protocol&quot;&gt;Joybus Protocol&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;emulation&quot;&gt;Emulation&lt;/h2&gt;

&lt;p&gt;There are many challenges in N64 emulation. It’s not just a matter of emulating the CPU. The RCP and RSP also need to be emulated.&lt;/p&gt;

&lt;p&gt;The RCP relies on microcode, which is driven using Display Lists. While this can be emulated on the instruction level, doing so is computationally intensive, even on modern hardware. It is more common to emulate the RCP at the microcode level, using what is known as High-Level Emulation (HLE).&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/UltraHLE&quot;&gt;UltraHLE&lt;/a&gt; was a great example of High-Level Emulation in action. This emulated achieved what was thought to be impossible - full speed N64 emulation on PC hardware. UltraHLE was designed to intercept C function calls, which were mapped to efficient implementations on the CPU, allowing games such as Ocarina of Time and Super Mario 64 to run efficiently on PC hardware in the late 90s and early 2000s.&lt;/p&gt;

&lt;p&gt;While High-Level Emulation works well for many games, especially first-party titles, it falls apart when a ROM uses non-standard microcode, an example being &lt;em&gt;Indiana Jones and the Infernal Machine&lt;/em&gt; and &lt;em&gt;Star Wars Episode I: Battle for Naboo&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;See this interesting write-up of the effort that went into reverse engineering some custom microcode that &lt;a href=&quot;https://en.wikipedia.org/wiki/Factor_5&quot;&gt;Factor 5&lt;/a&gt; developed towards the end of the N64’s commercial lifespan:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/gonetz/GLideN64/wiki/The-masterpiece-graphic-microcode-behind--the-Nintendo-64-version-of--Indiana-Jones-and-the-Infernal-Machine-and-Star-Wars-Episode-I:-Battle-for-Naboo&quot;&gt;The masterpiece graphic microcode behind the Nintendo 64 version of Indiana Jones and the Infernal Machine and Star Wars Episode I: Battle for Naboo
&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As for more modern emulators, I used a variety for testing:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/n64dev/cen64&quot;&gt;CEN64 (macOS and Linux)&lt;/a&gt; - I had to build this from source, but fortunately that is not difficult to do. Running a ROM will require a BIOS file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pifdata.bin&lt;/code&gt;. This is easy enough to find online. On macOS you will also need to install XQuartz.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://sixtyforce.com/&quot;&gt;sixtyforce (macOS)&lt;/a&gt; - A nice no-frills emulator that has good speed and compatibility. Registering is optional, but is nice to do if you want to unlock save state support and remove the watermark.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pj64-emu.com/&quot;&gt;Project64 (Windows)&lt;/a&gt; - A solid Windows-based emulator, with strong support for debugging, a highly configurable emulation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;dev-environment&quot;&gt;Dev Environment&lt;/h2&gt;

&lt;p&gt;There are a number of options for setting up an N64 development environment. If you’re comfortable using the original Windows SDK, this can be obtained from ultra64.ca:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://ultra64.ca/files/software/other/sdks/n64sdk.7z&quot;&gt;https://ultra64.ca/files/software/other/sdks/n64sdk.7z&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are several options for how you actually install and use this SDK. Some people choose to use a Windows XP or Windows 2000 VM. I found that it was more practical to install the SDK via WINE, running on a Linux VM.&lt;/p&gt;

&lt;p&gt;Unfortunately, using WINE is no longer viable recent versions of macOS, due to the removal of 32-bit support.&lt;/p&gt;

&lt;p&gt;Exactly how you set this up will depend on your desktop environment, etc. So my recommendation is to find a tutorial on YouTube that is suited to your setup.&lt;/p&gt;

&lt;h3 id=&quot;modern-sdk&quot;&gt;Modern SDK&lt;/h3&gt;

&lt;p&gt;If you’re using Linux, then another good option is to use CrashOveride95’s &lt;a href=&quot;https://crashoveride95.github.io/n64hbrew/modernsdk/index.html&quot;&gt;Modern SDK&lt;/a&gt;. This Linux-native SDK uses an up-to-date version of GCC, making significantly more sophisticated optimisations available. It also gives you the ability to use more modern C features.&lt;/p&gt;

&lt;p&gt;For my development environment, I was able to install the Modern SDK on the same Linux VM that I used for the official SDK running on WINE.&lt;/p&gt;

&lt;h3 id=&quot;hybrid-approach&quot;&gt;Hybrid Approach&lt;/h3&gt;

&lt;p&gt;Unfortunately, these two SDKs have different requirements as far as Makefiles and build steps are concerned.&lt;/p&gt;

&lt;p&gt;For this project, I wanted to support both SDKs, so I’ve included two Makefiles.&lt;/p&gt;

&lt;p&gt;The code has also been written to compile without warnings with both SDKs. This meant adopting a subset of C that works with both SDKs, and is not precisely defind by a single standard. While this is not ideal for code readability, it makes it possible to compare the performance and behaviour of code compiled under both SDKs, which in itself is an interesting exercise.&lt;/p&gt;

&lt;h2 id=&quot;real-hardware&quot;&gt;Real Hardware&lt;/h2&gt;

&lt;p&gt;Of course, no homebrew project is really complete until it’s running on real hardware.&lt;/p&gt;

&lt;p&gt;For this, I use an &lt;a href=&quot;https://krikzz.com/our-products/cartridges/ed64x7.html&quot;&gt;Everdrive-64 X7&lt;/a&gt; flash cartridge. The console is connected to a regular HDMI monitor using a &lt;a href=&quot;https://www.retrotink.com/product-page/2x-mini&quot;&gt;RetroTINK 2X-MINI&lt;/a&gt;, which you can see next to the classic &lt;em&gt;Nintendo Yellow&lt;/em&gt; controller:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/porting-asteroids-to-the-nintendo-64/everdrive-setup.jpeg&quot; alt=&quot;Everdrive-64 development setup&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Not only does this cart support loading ROMs via an SD-card, tt also includes a USB interface, allowing ROMs to be loaded directly from a PC.&lt;/p&gt;

&lt;p&gt;To upload ROMs via USB, I used buu342’s &lt;a href=&quot;https://github.com/buu342/N64-UNFLoader&quot;&gt;UNFLoader&lt;/a&gt;. The USB connection supports bi-directional communication, so it is possible to use UNFLoader for USB debugging.&lt;/p&gt;

&lt;h2 id=&quot;n64-operating-system&quot;&gt;N64 Operating System&lt;/h2&gt;

&lt;p&gt;Earlier consoles, such as the SNES or SEGA Mega Drive, often expected programmers to work directly with the hardware. Game code would be written in assembly language. While common assembly routines would often be provided as part of a development kit, the programmer was ultimately responsible for all aspects of the system. In some cases, specially customised cartridges would be used to enhance the capabilities of the console (e.g. the &lt;a href=&quot;https://en.wikipedia.org/wiki/Super_FX&quot;&gt;Super FX&lt;/a&gt; chip used in Star Fox).&lt;/p&gt;

&lt;p&gt;The Nintendo 64 offered a more modern approach. The official SDK provides a set of APIs, targeting the C programming language, that together constitute the Nintendo 64 Operating System. The N64 OS was small enough that it could be embedded in a ROM, requiring only a tiny amount of valuable ROM space. The OS provided APIs that address the following concerns:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Concurrency and message passing&lt;/li&gt;
  &lt;li&gt;Process/thread scheduling&lt;/li&gt;
  &lt;li&gt;Register and I/O access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see, this does not include graphics or audio!&lt;/p&gt;

&lt;h2 id=&quot;nusystem&quot;&gt;NuSystem&lt;/h2&gt;

&lt;p&gt;In practice, the N64 Operating System presents an extremely challenging development environment, so Nintendo also provided a set of higher level APIs for controlling the hardware. These are collectively known as NuSystem, often shortened to NuSys.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/porting-asteroids-to-the-nintendo-64/nusystem-architecture.png&quot; alt=&quot;NuSystem Architecture&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As an example of the functionality included in NuSys, these are a some of the functions provided for interacting with a Rumble Pak:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/**
 * Initiates operation of the Rumble Pak
 *
 * contNo   the Controller number
 * freq     the vibration frequency (1 - 256) where 1 is one pulse emitted
            every 256 frames and 256 is continuous movement
 * frame    the duration of the vibration in number of frames
 */
void nuContRmbStart(u32 contNo, u16 freq, u16 frame);

/**
 * Sets the operation mode of the Rumble Pak
 *
 * contNo   the Controller number
 * mode     the Rumble Pak operation mode
 *
 * valid modes:
 *   NU_CONT_RMB_MODE_DISABLE for disabled
 *   NU_CONT_RMB_MODE_ENABLE for enabled
 *   NU_CONT_RMB_MODE_AUTORUN for automatic recognition
 */
void nuContRmbModeSet(u32 contNo, u8 mode);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;All functions in NuSys are prefixed according to the subsystem they apply to. As you can see in the code snippet above, functions for working with the Rumble Pak are prefixed with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nuContRmb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It’s worth noting that the N64 OS and NuSys components are not strictly layered here. It’s entirely reasonable for a game to make use of NuSys, while still interacting directly with the N64 OS, when greater control is required.&lt;/p&gt;

&lt;h2 id=&quot;display-lists&quot;&gt;Display Lists&lt;/h2&gt;

&lt;p&gt;As mentioned earlier, graphics on the N64 is based on Display Lists (and in the case of audio, Audio Command Lists). This is how tasks are passed from the CPU to the RSP. This allows for a high degree of concurrent processing, whereby the RSP can process graphics and audio tasks while the CPU works on preparing subsequent tasks.&lt;/p&gt;

&lt;p&gt;To help illustrate this, I’ve prepared a sequence diagram. This shows how tasks move from the CPU to the RSP, making use of shared memory:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/porting-asteroids-to-the-nintendo-64/display-list-processing.png&quot; alt=&quot;Display List Processing&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;We can also see that the result of RSP processing a graphics task is a stream of low-level draw commands that are sent to the RDP. This is what drives rasterisation.&lt;/p&gt;

&lt;p&gt;The use of shared memory is an important consideration here. As mentioned earlier, the N64 has a Unified Memory Architecture (UMA), which means that the same memory is used by the CPU and RCP. The RCP actually serves as the memory controller for the N64, mediating access to system memory.&lt;/p&gt;

&lt;p&gt;When writing graphics code for the N64, it is important to recognise that the RSP is actively reading from the same region of memory used to prepare a Display List. Only when the RSP has completed a task can that memory be used for another task.&lt;/p&gt;

&lt;h2 id=&quot;sound-tools&quot;&gt;Sound Tools&lt;/h2&gt;

&lt;p&gt;There are two ways to prepare audio for inclusion in a ROM - one is to use the Windows-based Sound Tools, and the other is to use command line tools. After converting existing sound samples into a format that could be included in an N64 ROM, I battled with the Windows-based Sound Tools to marshall these into a form that could be embedded in a ROM.&lt;/p&gt;

&lt;p&gt;For all of this, I used the Sound Tools running on Linux, via WINE:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tristanpenman.com/images/porting-asteroids-to-the-nintendo-64/sound-tools@2x.png&quot; alt=&quot;Sound Tools running on Ubuntu Linux&quot; class=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Check out that version timestamp!&lt;/p&gt;

&lt;p&gt;My experience with the Sound Tools is that they’re incredibly painful to use. Clicking in the wrong location can easier do something completely unexpected. There are command line tools available, and I plan to use those next time I take on a N64 homebrew project.&lt;/p&gt;

&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;One of the interesting things to come out of this project is that the structure and quality of the code is better now than it was before I attempted to port it. All of the graphics code needed to be updated, after being modularised as a generic canvas API. The new API can be used to draw simple shapes, including the traditional Asteroids font.&lt;/p&gt;

&lt;p&gt;Audio is still lacking in many ways. With the differences in how audio is handled on the N64 (versus &lt;a href=&quot;https://github.com/libsdl-org/SDL_mixer&quot;&gt;SDL_mixer 2.0&lt;/a&gt;), it wasn’t clear how I could modularise this in the same way as the graphics/canvas APIs. The main reason for this is that audio files are embedded in the ROM via the linker, whereas the desktop version loads WAV files from disk.&lt;/p&gt;

&lt;p&gt;Overall, I’m keen to pick up another N64 homebrew project at some point, next time making use of the 3D capabilities of the console.&lt;/p&gt;
</description>
					<pubDate>Sat, 31 Dec 2022 00:00:00 +0000</pubDate>
					<link>https://tristanpenman.com/blog/posts/2022/12/31/porting-asteroids-to-the-nintendo-64/</link>
					<guid isPermaLink="true">https://tristanpenman.com/blog/posts/2022/12/31/porting-asteroids-to-the-nintendo-64/</guid>
				</item>
			
		
			
				<item>
					<title>Roll Your Own Ruby Type Checking: Part 1 </title>
					
					<description>&lt;div class=&quot;time&quot;&gt;Last Updated: 10 April 2023&lt;/div&gt;

&lt;p&gt;Earlier this year, I was talking with a colleague about &lt;a href=&quot;https://sorbet.org/&quot;&gt;Sorbet&lt;/a&gt; and other type checking features that are available for Ruby. Although we were with familiar with usage of gradual typing in other languages such as TypeScript and Python, neither of us really knew how this would work under the hood.&lt;/p&gt;

&lt;p&gt;This post begins with an example from the Sorbet documentation, and builds up the machinery to support a simplistic type checking implementation. While our implementation will not be production-ready like Sorbet, I hope that the exploration will be enlightening.&lt;/p&gt;

&lt;p&gt;Later posts will refine the type checking implementation, making it more versatile, and capable of being used with more sophisticated Ruby programs.&lt;/p&gt;

&lt;h2 id=&quot;type-checking&quot;&gt;Type Checking&lt;/h2&gt;

&lt;p&gt;But first, what is Type Checking. Or more specifically, what does it mean to check types?&lt;/p&gt;

&lt;p&gt;In a dynamically-typed language such as Ruby, the types of variables, parameters, etc, are not known until runtime. That is, they can vary depending on how your program is run. A variable that is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;String&lt;/code&gt; in some cases, may be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Numeric&lt;/code&gt; in other cases. The purpose of a type-checker is to detect bugs that may occur due to mis-matching types. An example of this is passing a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;String&lt;/code&gt; as an argument when a number is expected.&lt;/p&gt;

&lt;p&gt;Where does Sorbet fit into this?&lt;/p&gt;

&lt;h2 id=&quot;sorbet&quot;&gt;Sorbet&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://sorbet.org/&quot;&gt;Sorbet&lt;/a&gt; is a powerful type checker, built specifically for Ruby. It is implemented using Ruby language features, unlike other type checkers, which are often based on pre-processors and other tricks. The Sorbet ecosystem also provides plugins for IDE integration and additional development tooling.&lt;/p&gt;

&lt;p&gt;Let’s look at a simple example, showing how Sorbet is used… When we visit the Sorbet website we’re greeting with this little example, illustrating just how easy it is to add type checking to both new and existing code:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Main&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;extend&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Sig&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;sig&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Integer&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Hello, &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;!&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;length&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This could also be written more compactly:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Main&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;extend&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Sig&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;sig&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;name: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Integer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Hello, &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;!&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;length&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can see from this example that Sorbet provides type checking via a method called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sig&lt;/code&gt;, which looks a bit like an &lt;em&gt;annotation&lt;/em&gt; that you might see in a language such as Java.&lt;/p&gt;

&lt;p&gt;As is common in the Ruby ecosystem, this one line type annotation is built upon some non-trivial Ruby magic. In this post, we’ll attempt to re-invent our own annotation machinery, and use it to build a simple type checker inspired by Sorbet.&lt;/p&gt;

&lt;h2 id=&quot;repeater&quot;&gt;Repeater&lt;/h2&gt;

&lt;p&gt;For the examples in this post, we’ll work with a toy class called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Repeater&lt;/code&gt;. All this class does is repeat a string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;str&lt;/code&gt; a given number times &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt;, with each copy of the string separated by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;separator&lt;/code&gt;. Each version of this class will be given its own number, e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Repeater1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Repeater2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The original implementation looks like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Repeater&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This class is very easy to test from IRB, or from a script:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repeater&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;test&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;, &apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this case, we’ll get this output:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;test, test, test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now let’s say we want to add some functionality to this class, so that we print its positional arguments before the body is executed, and print out the return value before it is returned to the caller.&lt;/p&gt;

&lt;p&gt;Here’s the most direct way we could implement this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Repeater1&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;before: [&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;, &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;]&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;after: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now If we were to run the following:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repeater1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;test&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;, &apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It would produce:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;before: [&quot;test&quot;, 3]
after: test, test, test
test, test, test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If we now allow ourselves a little bit of indirection, this can be made more generic by lifting the method body into a lambda:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Repeater2&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;before: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# lambda literal&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;after: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;What is unusual about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Repeater2&lt;/code&gt; is that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;repeat&lt;/code&gt; method no longer uses the original parameter list. Instead, it uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*args&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**kwargs&lt;/code&gt;. These are called rest parameter, and allow us to capture and print all arguments using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;puts&lt;/code&gt;. The code from the original &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;repeat&lt;/code&gt; method is captured within a lambda literal, where we see the original parameter list.&lt;/p&gt;

&lt;p&gt;If we were to run the following line of code:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repeater2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;test&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;, &apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It will produce the same output as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Repeater1&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;before: [&quot;test&quot;, 3]
after: test, test, test
test, test, test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;hooks&quot;&gt;Hooks&lt;/h2&gt;

&lt;p&gt;Of course, we are working towards an annotation-based style, and would like to simplify &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Repeater2&lt;/code&gt; so that the code to run before and after the method body is more clearly separated. Ideally something like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;before&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;before: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;after&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;after: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this snippet, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before&lt;/code&gt; is a hook that is called with the arguments supplied to the method, but &lt;em&gt;before&lt;/em&gt; the body of the method is executed. And &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after&lt;/code&gt; is a hook that is called after the body is executed, with its return value.&lt;/p&gt;

&lt;p&gt;If we consider that Ruby class declarations are executed from top to bottom, and existing methods can be called from within the class body, then we see that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after&lt;/code&gt; could be provided by extending a module:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MyClass&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;extend&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hooks&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# methods from Hooks are available now&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ...&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;How does the Hooks module work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can see from the snippets above, that we need to define a module that provides &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after&lt;/code&gt; methods. We’ll call this module &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hooks&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after&lt;/code&gt; methods are very simple setters that store a lambda in a class variable, so that it can be used later. We’ll call this the hook &lt;em&gt;context&lt;/em&gt; for a method.&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Hooks&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;before&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@before&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;after&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@after&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tag&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;We can then use Ruby’s powerful meta-programming functionality to enhance method declaration, in such a way that the next method to be defined will execute within our new hook contexr.&lt;/p&gt;

&lt;p&gt;We’ll do this by implementing &lt;a href=&quot;https://apidock.com/ruby/Module/method_added&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;method_added&lt;/code&gt;&lt;/a&gt;, which is automatically called whenever a method is added to a class:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;method_added&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# short-circuit, to avoid infinite loop&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@before&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@after&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# reset hook context, but store current values&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;before&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@before&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@before&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;after&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@after&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@after&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# capture the original method&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;meth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;instance_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# wrap the original method with calls to before and after hooks&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;define_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;before&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;before&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;meth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;after&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;after&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://apidock.com/ruby/Module/define_method&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;define_method&lt;/code&gt;&lt;/a&gt; is used to replace the newly added method with a block that executes our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after&lt;/code&gt; hooks. We have to be careful to clear &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@before&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@after&lt;/code&gt; before calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;define_method&lt;/code&gt;, otherwise this code will keep triggering &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;method_added&lt;/code&gt;, and we’ll end up with a stack overflow.&lt;/p&gt;

&lt;p&gt;We can now implement &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Repeater3&lt;/code&gt;, extending the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hooks&lt;/code&gt; module:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Repeater3&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;extend&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Hooks&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;before&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;before: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;after&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;after: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Lets check that it works as expected, using the same test code as earlier:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repeater3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;test&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;, &apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This should produce exactly the same output:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;before: [&quot;test&quot;, 3]
after: test, test, test
test, test, test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;type-checking-1&quot;&gt;Type Checking&lt;/h2&gt;

&lt;p&gt;The next step is to develop this into a convenient, if somewhat primitive, type checking system.&lt;/p&gt;

&lt;p&gt;Our first thought may be to use our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hooks&lt;/code&gt; module to write bespoke type checking code like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;before&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;invalid str&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;invalid count&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Numeric&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;invalid separator&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nil?&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;after&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;begin&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;invalid return value&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;While this may work, it would be time-consuming, error prone, and not particularly easy to maintain. We would like to streamline this so that we write something more compact, like this:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;typedef&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Numeric&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;There’s a fair bit going on here. Firstly, we’re no longer using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;before&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;after&lt;/code&gt;. Instead we’re using a hook called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;typedef&lt;/code&gt;. Within the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;typedef&lt;/code&gt; call, we’re also using methods called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;returns&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is getting a bit closer to the interface offered by Sorbet:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;sig&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;ss&quot;&gt;str: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;count: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Integer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Integer&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;method-parameters&quot;&gt;Method Parameters&lt;/h2&gt;

&lt;p&gt;One of the interested problems here is that we’re using both positional parameters (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;str&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt;) and keyword parameters (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;separator&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Sorbet uses some additional metaprogramming magic to handle these, however, for the purposes of this experiment, we’ll keep things simple by handling positional and keyword parameters separately.&lt;/p&gt;

&lt;h2 id=&quot;types&quot;&gt;Types&lt;/h2&gt;

&lt;p&gt;What we end up with is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Types&lt;/code&gt; module that provides two methods, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;returns&lt;/code&gt; which can be use to specify the types for a method. These methods both return &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;self&lt;/code&gt;, which is what allows them to be chained.&lt;/p&gt;

&lt;p&gt;Here is the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Types&lt;/code&gt; module in all its glory:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;module&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;Types&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwarg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@kwarg_types&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kwarg_types&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@ret_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;typedef&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Allows typedef to be passed a block to execute within the context&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# of the Types module&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;method_added&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# short-circuit, to avoid infinite loop&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@arg_types&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@kwarg_types&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@ret_type&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# reset hook context, but store current values&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kwarg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@kwarg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@ret_type&lt;/span&gt;
    &lt;span class=&quot;vi&quot;&gt;@arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@kwarg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@ret_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# capture the original method&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;meth&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;instance_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# wrap the original method with type checks&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;define_method&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# check positional arguments&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each_with_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid type for arg &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;; expected: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;arg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

      &lt;span class=&quot;c1&quot;&gt;# check keyword arguments&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;kwarg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid type for kwarg &apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;`; expected &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;kwarg_types&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
          &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;kwargs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;type&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;meth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;c1&quot;&gt;# check return type&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Invalid return type, expected &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;unless&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;is_a?&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret_type&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;putting-it-together&quot;&gt;Putting it together&lt;/h2&gt;

&lt;p&gt;Now we can implement the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Repeater&lt;/code&gt; class using our home-grown type checking system:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Repeater4&lt;/span&gt;
  &lt;span class=&quot;kp&quot;&gt;extend&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Types&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;typedef&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Numeric&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;returns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;no&quot;&gt;String&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If we pass valid arguments, as per previous examples, we should see the expected output:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;test, test, test
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But we replace the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count&lt;/code&gt; argument with a string, we’ll see our type checking in action:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;begin&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Repeater4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;repeat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;3&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;separator: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;, &quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;rescue&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;StandardError&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Error: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The output should look something like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Error: Invalid type for arg in pos 1; expected: Numeric
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;If you take away anything from this post, hopefully it is that Ruby allows you to build annotation-like constructs, using features that are part of the language.&lt;/p&gt;

&lt;p&gt;These annotations can be evaluated when classes are loaded, and Ruby’s rich support for metaprogramming means that it is possible to do much more than simply annotate methods.&lt;/p&gt;

&lt;p&gt;As for the type checker we constructed in this post, it is currently very simplistic, and not particularly useful. However it lays the groundwork for future posts, in which we’ll develop a more complete type checker.&lt;/p&gt;

&lt;h3 id=&quot;code&quot;&gt;Code&lt;/h3&gt;

&lt;p&gt;All of the code for this post can be found in my &lt;a href=&quot;https://github.com/tristanpenman/ruby-type-checking&quot;&gt;ruby-type-checking&lt;/a&gt; repo on GitHub.&lt;/p&gt;
</description>
					<pubDate>Mon, 26 Dec 2022 00:00:00 +0000</pubDate>
					<link>https://tristanpenman.com/blog/posts/2022/12/26/roll-your-own-ruby-type-checking-part-1/</link>
					<guid isPermaLink="true">https://tristanpenman.com/blog/posts/2022/12/26/roll-your-own-ruby-type-checking-part-1/</guid>
				</item>
			
		
	</channel>
</rss>
