lib/adc: Log continuous updates in proto binary

- Log adc measurements in encoded proto binary format.
- Switch SPI bus baudrate hz variable to uint32.
- Use SPI peripheral managed CS pin for ADC spi transactions.
- Switch ADC SPI bus speed to 7.5mhz.
- Remove ADCTask elapsed time check to run as fast as possible.

Bug: b/323017707

Change-Id: I4a740820752dd50b280485443a8be377422227f7
Reviewed-on: https://pigweed-review.googlesource.com/c/gonk/+/188952
Reviewed-by: Eric Holland <hollande@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 8a504ba..222e8fc 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -51,6 +51,8 @@
 # Python Targets
 _gonk_python_packages = [ "//tools" ]
 
+_gonk_proto_packages = [ "//lib/adc:protos.python" ]
+
 # Pigweed Python packages to include
 _pigweed_python_packages = [ "$dir_pw_env_setup:core_pigweed_python_packages" ]
 
@@ -69,7 +71,7 @@
 # single super Python package for installation into the bootstrapped virtual
 # environment.
 pw_python_distribution("gonk_python_distribution") {
-  packages = _pigweed_python_packages
+  packages = _pigweed_python_packages + _gonk_proto_packages
   generate_setup_cfg = {
     name = "gonk-dist"
     version = "0.0.1"
diff --git a/applications/fpga_config/main.cc b/applications/fpga_config/main.cc
index 41829fe..5c48e8d 100644
--- a/applications/fpga_config/main.cc
+++ b/applications/fpga_config/main.cc
@@ -48,7 +48,7 @@
 FpgaControl fpga_control = FpgaControl(&pin_config);
 SpiFlash spi_flash =
     SpiFlash(FlashCS, /*baudrate=*/1000000, pin_config.flash_spi);
-Adc fpga_adc = Adc(pin_config.fpga_spi, /*fpga_spi_baudrate=*/1000000,
+Adc fpga_adc = Adc(Serial, pin_config.fpga_spi, /*fpga_spi_baudrate=*/7500000,
                    FpgaDspiCS, FpgaIoMode, FpgaIoReset, FpgaIoValid);
 
 void ice40_done_rising_isr() { PW_LOG_DEBUG("ICE40 Done: HIGH"); }
@@ -95,50 +95,15 @@
 }
 
 void ADCTask() {
-  static uint32_t last_update = millis();
-  static uint32_t this_update = millis();
-  static uint16_t update_count = 0;
-
-  this_update = millis();
-
-  // Check for serial input.
-  if (Serial.available()) {
-    int data_in = Serial.read();
-    // Press enter to restart the FPGA config.
-    if (data_in == '\n') {
-      PW_LOG_INFO("Restarting FPGA Config.");
-      current_task = &FpgaConfigTask;
+  pw::Status update_result = fpga_adc.UpdateContinuousMeasurements();
+  if (!update_result.ok()) {
+    PW_LOG_ERROR("UpdateContinuousMeasurements() failed");
+  } else {
+    pw::Status write_result = fpga_adc.WriteMeasurementPacket();
+    if (!write_result.ok()) {
+      PW_LOG_ERROR("WriteMeasurementPacket failed");
     }
   }
-
-  if (this_update > last_update + 1) {
-    PW_LOG_INFO("ADC update: %d", update_count);
-
-    pw::Status update_result = fpga_adc.UpdateContinuousMeasurements();
-    if (!update_result.ok()) {
-      PW_LOG_ERROR("UpdateContinuousMeasurements() failed");
-    } else {
-      PW_LOG_INFO(
-          "  vbus: %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, ",
-          fpga_adc.vbus_measurements_[0], fpga_adc.vbus_measurements_[1],
-          fpga_adc.vbus_measurements_[2], fpga_adc.vbus_measurements_[3],
-          fpga_adc.vbus_measurements_[4], fpga_adc.vbus_measurements_[5],
-          fpga_adc.vbus_measurements_[6], fpga_adc.vbus_measurements_[7],
-          fpga_adc.vbus_measurements_[8], fpga_adc.vbus_measurements_[9],
-          fpga_adc.vbus_measurements_[10]);
-      PW_LOG_INFO(
-          "vshunt: %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, ",
-          fpga_adc.vshunt_measurements_[0], fpga_adc.vshunt_measurements_[1],
-          fpga_adc.vshunt_measurements_[2], fpga_adc.vshunt_measurements_[3],
-          fpga_adc.vshunt_measurements_[4], fpga_adc.vshunt_measurements_[5],
-          fpga_adc.vshunt_measurements_[6], fpga_adc.vshunt_measurements_[7],
-          fpga_adc.vshunt_measurements_[8], fpga_adc.vshunt_measurements_[9],
-          fpga_adc.vshunt_measurements_[10]);
-    }
-
-    last_update = this_update;
-    update_count = (update_count + 1) % UINT16_MAX;
-  }
 }
 
 void CheckSpiFlash() {
@@ -162,6 +127,8 @@
     // Init ADCs
     fpga_adc.InitAdcs();
     fpga_adc.CheckAllAdcs();
+
+    PW_LOG_INFO("Switching to ADC binary log");
     fpga_adc.SetContinuousReadMode();
   } else {
     PW_LOG_INFO("Restarting FPGA Config.");
diff --git a/lib/adc/BUILD.gn b/lib/adc/BUILD.gn
index 0068028..af17a5d 100644
--- a/lib/adc/BUILD.gn
+++ b/lib/adc/BUILD.gn
@@ -15,11 +15,19 @@
 import("//build_overrides/pigweed.gni")
 
 import("$dir_pw_build/target_types.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
 
 config("default_config") {
   include_dirs = [ "public" ]
 }
 
+pw_proto_library("protos") {
+  sources = [ "adc_measurement.proto" ]
+  inputs = [ "adc_measurement.options" ]
+  prefix = "gonk_adc"
+  deps = [ "$dir_pw_protobuf:common_protos" ]
+}
+
 pw_source_set("adc") {
   public_configs = [ ":default_config" ]
   public = [ "public/gonk/adc.h" ]
@@ -33,6 +41,7 @@
     "$dir_pw_status",
   ]
   public_deps = [
+    ":protos.pwpb",
     "$dir_pw_bytes",
     "$dir_pw_third_party/arduino:arduino_core_sources",
     "//third_party:stm32duino_spi",
diff --git a/lib/adc/adc.cc b/lib/adc/adc.cc
index 441e1c4..091513e 100644
--- a/lib/adc/adc.cc
+++ b/lib/adc/adc.cc
@@ -5,8 +5,9 @@
 #include <stdint.h>
 
 #include "gonk/adc.h"
+#include "gonk_adc/adc_measurement.pwpb.h"
 
-#define PW_LOG_LEVEL PW_LOG_LEVEL_DEBUG
+#define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
 #define PW_LOG_MODULE_NAME "Adc"
 
 #include "pw_bytes/bit.h"
@@ -77,17 +78,17 @@
 constexpr pw::span<const uint8_t>
     kINA229RegisterByteSize(INA229RegisterByteSize);
 
-Adc::Adc(SPIClass &fpga_spi, uint16_t fpga_spi_baudrate, uint16_t fpga_cs_pin,
-         uint16_t fpga_mode_pin, uint16_t fpga_reset_pin,
+Adc::Adc(Stream &serial_stream, SPIClass &fpga_spi, uint32_t fpga_spi_baudrate,
+         uint16_t fpga_cs_pin, uint16_t fpga_mode_pin, uint16_t fpga_reset_pin,
          uint16_t fpga_valid_pin)
-    : fpga_spi_(fpga_spi), fpga_reset_(fpga_reset_pin),
+    : serial_(serial_stream), fpga_spi_(fpga_spi), fpga_reset_(fpga_reset_pin),
       fpga_mode_(fpga_mode_pin), fpga_valid_(fpga_valid_pin),
       fpga_cs_(fpga_cs_pin),
       spi_settings_(fpga_spi_baudrate, MSBFIRST, SPI_MODE1) {
   sample_read_index_ = 0;
+  measurement_timestamp_ = 0;
+  previous_timestamp_ = 0;
   sample_read_time_.fill(0);
-  vbus_measurements_.fill(0);
-  vshunt_measurements_.fill(0);
 }
 
 void Adc::SetReadWriteMode() {
@@ -225,15 +226,9 @@
   return INA229RegisterByteSize[adc_register];
 }
 
-void Adc::StartSpiTransaction() {
-  digitalWrite(fpga_cs_, LOW);
-  fpga_spi_.beginTransaction(spi_settings_);
-}
+void Adc::StartSpiTransaction() { fpga_spi_.beginTransaction(spi_settings_); }
 
-void Adc::EndSpiTransaction() {
-  digitalWrite(fpga_cs_, HIGH);
-  fpga_spi_.endTransaction();
-}
+void Adc::EndSpiTransaction() { fpga_spi_.endTransaction(); }
 
 void Adc::WriteAddress(uint32_t adc_address) {
   uint8_t address[3];
@@ -313,7 +308,10 @@
     return wait_result;
   }
 
-  for (int i = 0; i < kStreamingAdcCount; i++) {
+  previous_timestamp_ = measurement_timestamp_;
+  measurement_timestamp_ = micros();
+  measurement_delta_micros_ = measurement_timestamp_ - previous_timestamp_;
+  for (int i = 0; i < kMaxStreamingAdcCount; i++) {
     // Read VBUS
     std::array<std::byte, 3> vbus_read_buffer;
     const pw::Result<pw::ConstByteSpan> vbus_read_result =
@@ -323,10 +321,9 @@
       return vbus_read_result.status();
     }
     int32_t vbus_value = VoltageMeasurement(vbus_read_result.value());
-    vbus_measurements_[i] = vbus_value;
-    PW_LOG_INFO("Continuous Read ADC #%02d: VBUS   = %02x %02x %02x = %d", i,
-                vbus_read_result.value()[0], vbus_read_result.value()[1],
-                vbus_read_result.value()[2], vbus_value);
+    PW_LOG_DEBUG("Continuous Read ADC #%02d: VBUS   = %02x %02x %02x = %d", i,
+                 vbus_read_result.value()[0], vbus_read_result.value()[1],
+                 vbus_read_result.value()[2], vbus_value);
 
     // Read VSHUNT
     std::array<std::byte, 3> vshunt_read_buffer;
@@ -337,10 +334,17 @@
       return vshunt_read_result.status();
     }
     int32_t vshunt_value = VoltageMeasurement(vshunt_read_result.value());
-    vshunt_measurements_[i] = vshunt_value;
-    PW_LOG_INFO("Continuous Read ADC #%02d: VSHUNT = %02x %02x %02x = %d", i,
-                vshunt_read_result.value()[0], vshunt_read_result.value()[1],
-                vshunt_read_result.value()[2], vshunt_value);
+    PW_LOG_DEBUG("Continuous Read ADC #%02d: VSHUNT = %02x %02x %02x = %d", i,
+                 vshunt_read_result.value()[0], vshunt_read_result.value()[1],
+                 vshunt_read_result.value()[2], vshunt_value);
+
+    // Save this measurement.
+    measurements_[i].vbus_value_ = vbus_value;
+    measurements_[i].vshunt_value_ = vshunt_value;
+    for (size_t i = 0; i < 3; i++) {
+      measurements_[i].vbus_bytes_[i] = vbus_read_result.value()[i];
+      measurements_[i].vshunt_bytes_[i] = vshunt_read_result.value()[i];
+    }
   }
 
   // All data has been read: clear the pulse signal variable.
@@ -355,6 +359,57 @@
   return pw::OkStatus();
 }
 
+// Ensure the proto max size is == the expected kMaxStreamingAdcCount
+static_assert(Payload::kAdcMeasurementsMaxSize == kMaxStreamingAdcCount);
+
+Status Adc::WriteMeasurementPacket() {
+  std::array<std::byte, FramedProto::kMaxEncodedSizeBytes +
+                            (AdcMeasure::kMaxEncodedSizeBytes *
+                             Payload::kAdcMeasurementsMaxSize)>
+      packet_buffer;
+  FramedProto::MemoryEncoder packet(packet_buffer);
+  Status status = packet.WriteMagicStart(kFramedProtoMagicConstant);
+  if (!status.ok()) {
+    PW_LOG_ERROR("WriteMagicStart %d", status);
+    return status;
+  }
+
+  // Controls lifetime of payload.
+  {
+    auto payload = packet.GetPayloadEncoder();
+    status = payload.WriteTimestamp(measurement_delta_micros_);
+    if (!status.ok()) {
+      PW_LOG_ERROR("WriteTimestamp: %d", status);
+      return status;
+    }
+    for (size_t i = 0; i < kMaxStreamingAdcCount; i++) {
+      auto adc_encoder = payload.GetAdcMeasurementsEncoder();
+      status = adc_encoder.WriteVbusValue(measurements_[i].vbus_value_);
+      if (!status.ok()) {
+        PW_LOG_ERROR("WriteVbusValue: #%d %d", i, status);
+        return status;
+      }
+      status = adc_encoder.WriteVshuntValue(measurements_[i].vshunt_value_);
+      if (!status.ok()) {
+        PW_LOG_ERROR("WriteVshuntValue: #%d %d", i, status);
+        return status;
+      }
+    }
+  }
+
+  if (!packet.status().ok()) {
+    PW_LOG_ERROR("packet.status %d", status);
+    return packet.status();
+  }
+
+  // Write proto packet bytes over serial.
+  serial_.write(reinterpret_cast<const uint8_t *>(packet.data()),
+                packet.size());
+  serial_.flush();
+
+  return pw::OkStatus();
+}
+
 Status Adc::WriteRegister(uint8_t adc_number, uint8_t adc_register,
                           pw::ByteSpan write_buffer) {
   uint32_t adc_address = ADCAddressWrite(adc_number, adc_register);
@@ -479,12 +534,23 @@
       uint16_t adc_value = pw::bytes::ReadInOrder<uint16_t>(
           pw::endian::big, adc_config_result.value().data());
       PW_LOG_INFO("ADC #%02d: ADC Config = %x", adc_number, adc_value);
+    } else {
+      return adc_config_result.status();
     }
 
     // Read and log the VSHUNT register.
-    GetShuntVoltageMeasurement(adc_number);
+    const pw::Result<int32_t> vshunt_result =
+        GetShuntVoltageMeasurement(adc_number);
+    if (!vshunt_result.ok()) {
+      PW_LOG_ERROR("GetShuntVoltageMeasurement failed: %d",
+                   vshunt_result.status());
+    }
     // Read and log the VBUS register.
-    GetBusVoltageMeasurement(adc_number);
+    const pw::Result<int32_t> vbus_result =
+        GetBusVoltageMeasurement(adc_number);
+    if (!vbus_result.ok()) {
+      PW_LOG_ERROR("GetBusVoltageMeasurement failed: %d", vbus_result.status());
+    }
   }
 
   return pw::OkStatus();
diff --git a/lib/adc/adc_measurement.options b/lib/adc/adc_measurement.options
new file mode 100644
index 0000000..344e30d
--- /dev/null
+++ b/lib/adc/adc_measurement.options
@@ -0,0 +1,15 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+gonk.adc.Payload.adc_measurements max_count:11
\ No newline at end of file
diff --git a/lib/adc/adc_measurement.proto b/lib/adc/adc_measurement.proto
new file mode 100644
index 0000000..1f1f3e1
--- /dev/null
+++ b/lib/adc/adc_measurement.proto
@@ -0,0 +1,34 @@
+// Copyright 2024 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+syntax = "proto3";
+
+package gonk.adc;
+
+import "pw_protobuf_protos/common.proto";
+
+message AdcMeasure {
+  int32 vbus_value = 1;
+  int32 vshunt_value = 2;
+}
+
+message Payload {
+  repeated AdcMeasure adc_measurements = 1;
+  uint32 timestamp = 2;
+}
+
+message FramedProto {
+  fixed32 magic_start = 1;
+
+  Payload payload = 2;
+}
\ No newline at end of file
diff --git a/lib/adc/public/gonk/adc.h b/lib/adc/public/gonk/adc.h
index 510b9a3..ee0393f 100644
--- a/lib/adc/public/gonk/adc.h
+++ b/lib/adc/public/gonk/adc.h
@@ -14,13 +14,15 @@
 
 namespace gonk::adc {
 
+const uint32_t kFramedProtoMagicConstant = 0x7EAABB99;
 const uint8_t kTotalAdcCount = 11;
-const uint8_t kStreamingAdcCount = 11;
+const uint8_t kMaxStreamingAdcCount = 11;
 
 class Adc {
 public:
-  Adc(SPIClass &fpga_spi, uint16_t fpga_spi_baudrate, uint16_t fpga_cs_pin,
-      uint16_t fpga_mode_pin, uint16_t fpga_reset_pin, uint16_t fpga_valid_pin);
+  Adc(Stream &serial_stream, SPIClass &fpga_spi, uint32_t fpga_spi_baudrate,
+      uint16_t fpga_cs_pin, uint16_t fpga_mode_pin, uint16_t fpga_reset_pin,
+      uint16_t fpga_valid_pin);
 
   Status InitAdcs();
   Status CheckAllAdcs();
@@ -28,6 +30,7 @@
   void SetReadWriteMode();
   void SetContinuousReadMode();
   Status UpdateContinuousMeasurements();
+  Status WriteMeasurementPacket();
 
   // Writing and reading data over SPI.
   void StartSpiTransaction();
@@ -51,15 +54,24 @@
   pw::Result<int32_t> GetShuntVoltageMeasurement(uint8_t adc_number);
   pw::Result<int32_t> GetBusVoltageMeasurement(uint8_t adc_number);
 
-  std::array<int32_t, kStreamingAdcCount> vbus_measurements_;
-  std::array<int32_t, kStreamingAdcCount> vshunt_measurements_;
-
 private:
+  struct Measurement {
+    std::array<std::byte, 3> vbus_bytes_;
+    std::array<std::byte, 3> vshunt_bytes_;
+    int32_t vbus_value_;
+    int32_t vshunt_value_;
+  };
+  std::array<Measurement, kMaxStreamingAdcCount> measurements_;
+  uint32_t previous_timestamp_;
+  uint32_t measurement_timestamp_;
+  uint32_t measurement_delta_micros_;
+
   uint16_t fpga_reset_;
   uint16_t fpga_mode_;
   uint16_t fpga_valid_;
   uint16_t fpga_cs_;
 
+  Stream &serial_;
   SPIClass &fpga_spi_;
   SPISettings spi_settings_;
 
diff --git a/lib/pin_config/pin_config.cc b/lib/pin_config/pin_config.cc
index ae70ebe..1dc187d 100644
--- a/lib/pin_config/pin_config.cc
+++ b/lib/pin_config/pin_config.cc
@@ -12,7 +12,8 @@
 
 namespace gonk::pin_config {
 
-PinConfig::PinConfig() : fpga_spi(FpgaDspiMOSI, FpgaDspiMISO, FpgaDspiCLK) {
+PinConfig::PinConfig()
+    : fpga_spi(FpgaDspiMOSI, FpgaDspiMISO, FpgaDspiCLK, FpgaDspiCS) {
   flash_spi = SPIClass(FlashMOSI, FlashMISO, FlashCLK);
 }
 
diff --git a/lib/spi_flash/public/gonk/spi_flash.h b/lib/spi_flash/public/gonk/spi_flash.h
index f84d842..f9e4c62 100644
--- a/lib/spi_flash/public/gonk/spi_flash.h
+++ b/lib/spi_flash/public/gonk/spi_flash.h
@@ -20,7 +20,7 @@
     uint8_t product_version;
   };
 
-  SpiFlash(uint16_t flash_cs, int baudrate, SPIClass &flash_spi);
+  SpiFlash(uint16_t flash_cs, uint32_t baudrate, SPIClass &flash_spi);
 
   Status WriteEnable();
   Status WriteDisable();
diff --git a/lib/spi_flash/spi_flash.cc b/lib/spi_flash/spi_flash.cc
index 9202ed1..8d75555 100644
--- a/lib/spi_flash/spi_flash.cc
+++ b/lib/spi_flash/spi_flash.cc
@@ -16,7 +16,7 @@
 
 namespace gonk::spi_flash {
 
-SpiFlash::SpiFlash(uint16_t flash_cs, int baudrate, SPIClass &flash_spi)
+SpiFlash::SpiFlash(uint16_t flash_cs, uint32_t baudrate, SPIClass &flash_spi)
     : flash_cs_(flash_cs), spi_settings_(baudrate, MSBFIRST, SPI_MODE0),
       flash_spi_(flash_spi) {}
 
diff --git a/targets/stm32f730r8tx_arduino/init.cc b/targets/stm32f730r8tx_arduino/init.cc
index 437ee3d..eea9aa9 100644
--- a/targets/stm32f730r8tx_arduino/init.cc
+++ b/targets/stm32f730r8tx_arduino/init.cc
@@ -18,4 +18,4 @@
 
 // Arduino target specific init. pw_sys_io_arduino_Init() normally calls
 // Serial.begin(115200). Instead, we init serial to a higher baudrate here.
-extern "C" void pw_arduino_Init() { Serial.begin(1000000); }
+extern "C" void pw_arduino_Init() { Serial.begin(2000000); }
diff --git a/tools/BUILD.gn b/tools/BUILD.gn
index 0b04cfe..3b8c8f1 100644
--- a/tools/BUILD.gn
+++ b/tools/BUILD.gn
@@ -34,6 +34,7 @@
     "$dir_pw_build/py",
     "$dir_pw_cli/py",
     "$dir_pw_presubmit/py",
+    "//lib/adc:protos.python",
   ]
   pylintrc = "$dir_pigweed/.pylintrc"
 }
diff --git a/tools/gonk_tools/write_fpga.py b/tools/gonk_tools/write_fpga.py
index a3852f0..8e6f15c 100644
--- a/tools/gonk_tools/write_fpga.py
+++ b/tools/gonk_tools/write_fpga.py
@@ -14,6 +14,7 @@
 """Write a binary over serial to provision the Gonk FPGA."""
 
 import argparse
+from datetime import datetime
 from itertools import islice
 import operator
 from pathlib import Path
@@ -21,9 +22,15 @@
 import time
 from typing import Optional
 
+import serial
 from serial import Serial
 from serial.tools.list_ports import comports
-from serial.tools.miniterm import Miniterm
+from serial.tools.miniterm import Miniterm, Transform
+
+import pw_cli.color
+from gonk_adc.adc_measurement_pb2 import FramedProto
+
+_COLOR = pw_cli.color.colors()
 
 
 def _parse_args():
@@ -38,7 +45,7 @@
         '-b',
         '--baudrate',
         type=int,
-        default=1000000,
+        default=2000000,
         help='Sets the baudrate for serial communication.',
     )
     parser.add_argument(
@@ -55,6 +62,10 @@
         type=Path,
         help='FPGA Bitstream file.',
     )
+    parser.add_argument(
+        '--skip-config',
+        action='store_true',
+    )
     return parser.parse_args()
 
 
@@ -84,6 +95,9 @@
 FILE_START_BYTES = bytes.fromhex(FILE_START_STR)
 SYNC_START_BYTES = bytes.fromhex(SYNC_START_STR)
 
+BIN_LOG_SYNC_START_STR = '0d 99 bb aa 7e'
+BIN_LOG_SYNC_START_BYTES = bytes.fromhex(BIN_LOG_SYNC_START_STR)
+
 
 class IncorrectBinaryFormat(Exception):
     """Exception raised when FPGA bitstream file is in an unexpected format."""
@@ -106,12 +120,113 @@
         yield batch
 
 
+class HandleBinaryData(Transform):
+    """Miniterm transform to handle incoming byte data."""
+    def __init__(self) -> None:
+        self.include_binary = True
+        self.data = bytes()
+        self.timestamp_prefix = ' timestamp: '
+        self.vbus_prefix = ' vbus:'
+        self.vshunt_prefix = ' vshunt:'
+        self.start_time = time.time()
+        self.time_format = '%Y%m%d %H:%M:%S.%f'
+        self.binary_format_started = False
+
+    def rx(self, text, data=None):
+        """text received from serial port"""
+        if not data:
+            return text
+
+        # Concat data with any existing captures
+        self.data = self.data + data
+
+        # Check for binary start bytes.
+        if BIN_LOG_SYNC_START_BYTES in self.data:
+            self.binary_format_started = True
+        if not self.binary_format_started:
+            return text
+
+        # Check for a complete proto capture.
+        sections = self.data.split(BIN_LOG_SYNC_START_BYTES, maxsplit=2)
+        if len(sections) < 3:
+            # Don't print anything / filter out this text
+            return ''
+
+        # Section 0 contains previous bytes not between two proto start bytes.
+        _discard_bytes = sections[0]
+        # Section 1 is a complete proto packet.
+        proto_bytes = BIN_LOG_SYNC_START_BYTES + sections[1]
+
+        # Done, reset self.data to the remaining bytes minus the above packet.
+        self.data = BIN_LOG_SYNC_START_BYTES + sections[2]
+
+        # Parse the proto
+        try:
+            framed_proto = FramedProto()
+            framed_proto.ParseFromString(proto_bytes)
+            vbus_values = []
+            vshunt_values = []
+            for adc_measure in framed_proto.payload.adc_measurements:
+                vbus_values.append(adc_measure.vbus_value)
+                vshunt_values.append(adc_measure.vshunt_value)
+
+            # TODO(tonymd): Use Python logging and separate output file
+            output = [
+                (datetime.now().strftime(self.time_format) + '.' +
+                 str(len(proto_bytes)) + '.'),
+                self.timestamp_prefix,
+                str(framed_proto.payload.timestamp),
+                self.vbus_prefix,
+                ','.join(str(value) for value in vbus_values),
+                self.vshunt_prefix,
+                ','.join(str(value) for value in vshunt_values),
+            ]
+            return ' '.join(output) + '\n'
+        except FramedProto.DecodeError:
+            # TODO(tonymd): Handle failed packets.
+            return 'FramedProto.DecodeError\n'
+
+    def tx(self, text):
+        """text to be sent to serial port"""
+        return text
+
+    def echo(self, text):
+        """text to be sent but displayed on console"""
+        return text
+
+
+class MinitermBinary(Miniterm):
+    def reader(self):
+        """loop and copy serial->console"""
+        # pylint: disable=too-many-nested-blocks
+        try:
+            while self.alive and self._reader_alive:
+                # read all that is there or wait for one byte
+                data = self.serial.read(self.serial.in_waiting or 1)
+                if data:
+                    if self.raw:
+                        self.console.write_bytes(data)
+                    else:
+                        text = self.rx_decoder.decode(data)
+                        for transformation in self.rx_transformations:
+                            if hasattr(transformation, 'include_binary'):
+                                text = transformation.rx(text, data)
+                            else:
+                                text = transformation.rx(text)
+                        self.console.write(text)
+        except serial.SerialException:
+            self.alive = False
+            self.console.cancel()
+            raise  # XXX handle instead of re-raise?
+
+
 def main(
     baudrate: int,
     bitstream_file: Path,
     port: Optional[str] = None,
     product: Optional[str] = None,
     serial_number: Optional[str] = None,
+    skip_config: bool = False,
 ) -> int:
     """Write a bitstream file over serial while monitoring output."""
 
@@ -134,7 +249,7 @@
     serial_instance = Serial(port=port, baudrate=baudrate, timeout=320)
 
     # Use pyserial miniterm to monitor recieved data.
-    miniterm = Miniterm(
+    miniterm = MinitermBinary(
         serial_instance,
         echo=False,
         eol='crlf',
@@ -142,24 +257,27 @@
     )
     # Use Ctrl-C as the exit character. (Miniterm default is Ctrl-])
     miniterm.exit_character = chr(0x03)
-    miniterm.set_rx_encoding('utf-8')
+    miniterm.set_rx_encoding('utf-8', errors='backslashreplace')
     miniterm.set_tx_encoding('utf-8')
 
     # Start monitoring serial data.
     miniterm.start()
 
-    # Wait a couple seconds to print early log messages from Gonk.
-    time.sleep(2)
+    if not skip_config:
+        # Wait a couple seconds to print early log messages from Gonk.
+        time.sleep(2)
 
-    # Write out the bitstream in batches.
-    print('Sending bitstream...')
-    written_bytes: int = 0
-    for byte_batch in batched(bitstream_bytes, 8):
-        result = serial_instance.write(byte_batch)
-        if result:
-            written_bytes += result
-        serial_instance.flush()
-    print(f'Done sending bitstream. Wrote {written_bytes}')
+        # Write out the bitstream in batches.
+        print('Sending bitstream...')
+        written_bytes: int = 0
+        for byte_batch in batched(bitstream_bytes, 8):
+            result = serial_instance.write(byte_batch)
+            if result:
+                written_bytes += result
+            serial_instance.flush()
+        print(f'Done sending bitstream. Wrote {written_bytes}')
+
+    miniterm.rx_transformations.append(HandleBinaryData())
 
     # Wait for ctrl-c, then shutdown miniterm.
     try: