Merged logging with HDLC, log and ADC protos
- New device log handler: proto_over_hdlc.cc. This sends log proto
entries over serial in an HDLC frame.
- fpga_config/main.cc adds an ADCIdleTask to allow starting and
stopping adc logging when pressing enter.
- Remove log statements from interrupt handlers.
- Tweak the kFramedProtoMagicConstant to remove the `~`
character (0x7E) which is used in HDLC.
- Python changes:
- Refactor serial parsing to separate ADC proto updates from HDLC
wrapped log messages.
- Binary data functions added to help with read/write buffers and
parse varint encoding.
Bug: b/323017707
Change-Id: Ib6e1804be497b2c3d8aaf5657bd2546e07ddbe61
Reviewed-on: https://pigweed-review.googlesource.com/c/gonk/+/190510
Reviewed-by: Eric Holland <hollande@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
Reviewed-by: Ted Pudlik <tpudlik@google.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/README.md b/README.md
index 02a38fc..af729c0 100644
--- a/README.md
+++ b/README.md
@@ -89,7 +89,20 @@
1. Write an FPGA bitstream with the `write_fpga.py` script:
```sh
- python ./tools/gonk_tools/write_fpga.py --bitstream-file ./out/gn/obj/fpga/toplevel/toplevel.bin
+ python ./tools/gonk_tools/write_fpga.py \
+ --bitstream-file ./out/gn/obj/fpga/toplevel/toplevel.bin
+ --database ./out/gn/arduino_size_optimized/obj/applications/fpga_config/bin/fpga_config.elf \
+ --log-to-stderr
+ ```
+
+ You can redirect host and device logs to separate files with:
+
+ ```sh
+ python ./tools/gonk_tools/write_fpga.py \
+ --bitstream-file ./out/gn/obj/fpga/toplevel/toplevel.bin
+ --database ./out/gn/arduino_size_optimized/obj/applications/fpga_config/bin/fpga_config.elf \
+ --host-logfile gonk-host-logs.txt \
+ --device-logfile gonk-device-logs.txt
```
### Alternative: Flash with BlackMagic Probe
diff --git a/applications/fpga_config/main.cc b/applications/fpga_config/main.cc
index 6492d01..1699362 100644
--- a/applications/fpga_config/main.cc
+++ b/applications/fpga_config/main.cc
@@ -51,15 +51,14 @@
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"); }
-
void (*current_task)();
} // namespace
void IdleTask();
void FpgaConfigTask();
-void ADCTask();
+void ADCIdleTask();
+void ADCReadTask();
void IdleTask() {
static uint32_t last_update = millis();
@@ -94,7 +93,51 @@
}
}
-void ADCTask() {
+void ADCIdleTask() {
+ static uint32_t last_update = millis();
+ static uint32_t this_update = millis();
+ static uint16_t update_count = 0;
+
+ this_update = millis();
+
+ if (Serial.available()) {
+ int data_in = Serial.read();
+ if (data_in == '\n') {
+ PW_LOG_INFO("Starting ADC Continuous Reads.");
+ fpga_adc.SetContinuousReadMode();
+ current_task = &ADCReadTask;
+ }
+ }
+
+ // Output an idle state heartbeat message each second.
+ if (this_update > last_update + 1000) {
+ PW_LOG_INFO("ADC Idle");
+
+ last_update = this_update;
+ update_count = (update_count + 1) % UINT16_MAX;
+
+ // Toggle status LED each loop.
+ if (update_count % 2 == 0) {
+ digitalWrite(StatusLed, HIGH);
+ } else {
+ digitalWrite(StatusLed, LOW);
+ }
+ }
+}
+
+void ADCReadTask() {
+ // Check for serial input.
+ if (Serial.available()) {
+ int data_in = Serial.read();
+ // Press enter to stop.
+ if (data_in == '\n') {
+ PW_LOG_INFO("Stopping ADC Continuous Reads.");
+ fpga_adc.SetReadWriteMode();
+ current_task = &ADCIdleTask;
+ return;
+ }
+ }
+
pw::Status update_result = fpga_adc.UpdateContinuousMeasurements();
if (!update_result.ok()) {
PW_LOG_ERROR("UpdateContinuousMeasurements() failed");
@@ -121,7 +164,7 @@
auto result = fpga_control.StartConfig();
if (result.ok()) {
// FPGA has been configure successfully.
- current_task = &ADCTask;
+ current_task = &ADCReadTask;
// Check if SPI flash is readable.
CheckSpiFlash();
// Init ADCs
@@ -130,7 +173,6 @@
// Select the first five ADC channels.
fpga_adc.SelectContinuousReadAdcs(0b11111);
- PW_LOG_INFO("Switching to ADC binary log");
fpga_adc.SetContinuousReadMode();
} else {
PW_LOG_INFO("Restarting FPGA Config.");
@@ -139,10 +181,6 @@
}
int main() {
- // Debug interrupt to watch when ICE40Done goes high.
- attachInterrupt(/*pin=*/ICE40Done, /*callback=*/&ice40_done_rising_isr,
- /*mode=*/HIGH);
-
pin_config.Init();
pin_config.InitFpgaPins();
delay(500);
diff --git a/lib/adc/adc.cc b/lib/adc/adc.cc
index 40ed56e..b95516b 100644
--- a/lib/adc/adc.cc
+++ b/lib/adc/adc.cc
@@ -24,10 +24,7 @@
volatile uint8_t fpga_valid_pulse_ = 0;
-void io_valid_rising_isr() {
- fpga_valid_pulse_ = 1;
- PW_LOG_DEBUG("IO Valid: HIGH");
-}
+void io_valid_rising_isr() { fpga_valid_pulse_ = 1; }
void ClearValidPulse() { fpga_valid_pulse_ = 0; }
diff --git a/lib/adc/public/gonk/adc.h b/lib/adc/public/gonk/adc.h
index 9607fd6..9ab9ed0 100644
--- a/lib/adc/public/gonk/adc.h
+++ b/lib/adc/public/gonk/adc.h
@@ -14,7 +14,7 @@
namespace gonk::adc {
-const uint32_t kFramedProtoMagicConstant = 0x7EAABB99;
+const uint32_t kFramedProtoMagicConstant = 0x7DAABB99;
const uint8_t kTotalAdcCount = 11;
const uint8_t kMaxStreamingAdcCount = 11;
diff --git a/lib/log_backends/BUILD.gn b/lib/log_backends/BUILD.gn
new file mode 100644
index 0000000..8a07725
--- /dev/null
+++ b/lib/log_backends/BUILD.gn
@@ -0,0 +1,28 @@
+# 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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("proto_over_hdlc") {
+ sources = [ "proto_over_hdlc.cc" ]
+ deps = [
+ "$dir_pw_hdlc:encoder",
+ "$dir_pw_log:proto_utils",
+ "$dir_pw_log_tokenized:handler.facade",
+ "$dir_pw_stream:sys_io_stream",
+ dir_pw_span,
+ ]
+}
diff --git a/lib/log_backends/proto_over_hdlc.cc b/lib/log_backends/proto_over_hdlc.cc
new file mode 100644
index 0000000..87867fb
--- /dev/null
+++ b/lib/log_backends/proto_over_hdlc.cc
@@ -0,0 +1,63 @@
+// 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.
+
+// This function serves as a backend for pw_tokenizer / pw_log_tokenized that
+// encodes tokenized logs as Base64 and writes them using HDLC.
+
+#include "pw_bytes/endian.h"
+#include "pw_bytes/span.h"
+#include "pw_hdlc/encoder.h"
+#include "pw_log/proto_utils.h"
+#include "pw_log_tokenized/handler.h"
+#include "pw_log_tokenized/metadata.h"
+#include "pw_span/span.h"
+#include "pw_stream/sys_io_stream.h"
+#include "pw_string/string.h"
+#include "pw_tokenizer/tokenize.h"
+
+namespace pw::log_tokenized {
+namespace {
+
+inline constexpr int kLogProtoHdlcAddress = 1;
+std::array<std::byte, 512> log_encode_buffer;
+
+stream::SysIoWriter writer;
+
+// Create a log encode failure message in place of a proper crash handler.
+constexpr uint32_t kLogEncodeFailToken =
+ PW_TOKENIZE_STRING("pw::log::EncodeTokenizedLog failed");
+constexpr std::array<std::byte, 4> kLogEncodeFailBytes =
+ pw::bytes::CopyInOrder(pw::endian::little, kLogEncodeFailToken);
+
+} // namespace
+
+// Proto encodes tokenized logs and writes them to pw::sys_io as HDLC frames.
+extern "C" void pw_log_tokenized_HandleLog(uint32_t payload,
+ const uint8_t log_buffer[],
+ size_t size_bytes) {
+ pw::log_tokenized::Metadata metadata = payload;
+
+ Result<ConstByteSpan> encoded_log_result = pw::log::EncodeTokenizedLog(
+ metadata, log_buffer, size_bytes, /*timestamp=*/0, log_encode_buffer);
+
+ if (encoded_log_result.ok()) {
+ // HDLC-encode the encoded proto via a SysIoWriter.
+ hdlc::WriteUIFrame(kLogProtoHdlcAddress, encoded_log_result.value(),
+ writer);
+ } else {
+ hdlc::WriteUIFrame(kLogProtoHdlcAddress, kLogEncodeFailBytes, writer);
+ }
+}
+
+} // namespace pw::log_tokenized
diff --git a/targets/stm32f730r8tx_arduino/target_toolchains.gni b/targets/stm32f730r8tx_arduino/target_toolchains.gni
index b0fe0e2..15c756d 100644
--- a/targets/stm32f730r8tx_arduino/target_toolchains.gni
+++ b/targets/stm32f730r8tx_arduino/target_toolchains.gni
@@ -44,7 +44,8 @@
# Facade backends
pw_assert_BACKEND = dir_pw_assert_log
- pw_log_BACKEND = dir_pw_log_basic
+ pw_log_BACKEND = dir_pw_log_tokenized
+ pw_log_tokenized_HANDLER_BACKEND = "//lib/log_backends:proto_over_hdlc"
pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
"$dir_pw_sync_baremetal:interrupt_spin_lock"
pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
diff --git a/tools/BUILD.gn b/tools/BUILD.gn
index 3b8c8f1..d7027cd 100644
--- a/tools/BUILD.gn
+++ b/tools/BUILD.gn
@@ -23,17 +23,25 @@
]
sources = [
"gonk_tools/__init__.py",
+ "gonk_tools/binary_handler.py",
"gonk_tools/build_project.py",
"gonk_tools/find_serial_port.py",
"gonk_tools/flash.py",
+ "gonk_tools/gonk_log_stream.py",
"gonk_tools/presubmit_checks.py",
"gonk_tools/write_fpga.py",
]
+ tests = [ "binary_handler_test.py" ]
python_deps = [
"$dir_pw_arduino_build/py",
"$dir_pw_build/py",
"$dir_pw_cli/py",
+ "$dir_pw_console/py",
+ "$dir_pw_hdlc/py",
+ "$dir_pw_log/py",
+ "$dir_pw_log_tokenized/py",
"$dir_pw_presubmit/py",
+ "$dir_pw_tokenizer/py",
"//lib/adc:protos.python",
]
pylintrc = "$dir_pigweed/.pylintrc"
diff --git a/tools/binary_handler_test.py b/tools/binary_handler_test.py
new file mode 100644
index 0000000..c89b1f2
--- /dev/null
+++ b/tools/binary_handler_test.py
@@ -0,0 +1,175 @@
+# 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.
+"""Tests for gonk binary data"""
+
+import unittest
+
+from gonk_tools.binary_handler import (
+ BIN_LOG_SYNC_START_BYTES,
+ BIN_LOG_SYNC_START_STR,
+ BytearrayLoop,
+ decode_one_varint,
+)
+from gonk_tools.gonk_log_stream import GonkLogStream
+
+# pylint: disable=line-too-long
+_SAMPLE_ENCODED_LOGS = [
+ b'~$llsrqA==E\xc4\xe2\x93~',
+ b'~$w4ZyKAI=0\x8f\xbf!~',
+ b'~$w4ZyKAQ=i\xa4\xa3~',
+ b'~$w4ZyKAY=a\x9d}]k~',
+ b'~$w4ZyKAg=\x86\xb8*~',
+ b'~$YNjZuQ==X\xb2\xc1@~',
+ b'~$J+09dYBA\xb2\xf8^~',
+]
+
+_SAMPLE_PROTO_PACKETS = [
+ bytes.fromhex(
+ f'{BIN_LOG_SYNC_START_STR}122310d380ef020a04080010030a04080010020a040800104e0a040845104f0a0408001002'
+ ),
+ bytes.fromhex(
+ f'{BIN_LOG_SYNC_START_STR}122110bf010a04080010030a04080010020a040800104e0a04080010020a040800104f'
+ ),
+ bytes.fromhex(
+ f'{BIN_LOG_SYNC_START_STR}122a10b9010a04080010030a04080010020a04080010030a04080010020a0d080010b6ffffffffffffffff01'
+ ),
+ bytes.fromhex(
+ f'{BIN_LOG_SYNC_START_STR}122a10d1010a04080010030a04080010020a04084710030a0d080010b7ffffffffffffffff010a0408001002'
+ ),
+ bytes.fromhex(
+ f'{BIN_LOG_SYNC_START_STR}123310df010a04080010030a0d080010b6ffffffffffffffff010a04080010030a0d080010b7ffffffffffffffff010a0408001002'
+ ),
+ bytes.fromhex(
+ f'{BIN_LOG_SYNC_START_STR}127210ffffffff0f0a04080010030a04080010020a04080010030a04080010020a04080010020a0d080010b7ffffffffffffffff010a0d080010b7ffffffffffffffff010a04084710020a04084710030a04080010030a04080010020a1608ffffffffffffffffff0110ffffffffffffffffff01'
+ ),
+ bytes.fromhex(
+ f'{BIN_LOG_SYNC_START_STR}127b10ffffffff0f0a04080010030a04080010020a04080010030a04080010020a0d080010b6ffffffffffffffff010a0d080010b7ffffffffffffffff010a0d080010b7ffffffffffffffff010a04080010020a04080010030a04080010030a04080010020a1608ffffffffffffffffff0110ffffffffffffffffff01'
+ ),
+ bytes.fromhex(
+ f'{BIN_LOG_SYNC_START_STR}127210ffffffff0f0a04080010030a04080010020a04080010030a04084510020a04080010020a04080010020a0d080010b7ffffffffffffffff010a04084610020a04080010030a04080010030a0d084810b6ffffffffffffffff010a1608ffffffffffffffffff0110ffffffffffffffffff01'
+ ),
+]
+_SAMPLE_LONG_PROTO_PACKETS = [
+ # Artificially increased size > 127 (uses two bytes for the packet size varint)
+ bytes.fromhex(
+ f'{BIN_LOG_SYNC_START_STR}12F20110ffffffff0f0a04080010030a04080010020a04080010030a04084510020a04080010020a04080010020a0d080010b7ffffffffffffffff010a04084610020a04080010030a04080010030a0d084810b6ffffffffffffffff010a1608ffffffffffffffffff0110ffffffffffffffffff0110ffffffff0f0a04080010030a04080010020a04080010030a04084510020a04080010020a04080010020a0d080010b7ffffffffffffffff010a04084610020a04080010030a04080010030a0d084810b6ffffffffffffffff010a1608ffffffffffffffffff0110ffffffffffffffffff01ffffffffffffffffff01ffffffff'
+ ),
+]
+
+
+# pylint: enable=line-too-long
+class TestHandleBinaryData(unittest.TestCase):
+ """Tests for binary log data."""
+ def setUp(self):
+ # Show large diffs
+ self.maxDiff = None # pylint: disable=invalid-name
+
+ def test_gonk_stream_write(self) -> None:
+ """Test GonkLogStream.write handles protos and logs correctly."""
+ log_data = b''.join([
+ _SAMPLE_ENCODED_LOGS[1],
+ ])
+ proto_data1 = _SAMPLE_LONG_PROTO_PACKETS[0]
+
+ gl = GonkLogStream()
+ gl.write(log_data)
+ # No proto bytes yet.
+ self.assertEqual(None, gl.remaining_proto_bytes)
+
+ gl.write(proto_data1 + log_data)
+ # One log data section should be in the log_buffer
+ self.assertEqual(log_data, gl.log_buffer.getbuffer())
+ # One proto packet should be consumed.
+ self.assertEqual(None, gl.remaining_proto_bytes)
+ self.assertEqual(log_data, gl.input_buffer.getbuffer())
+
+ # Write a third log
+ gl.write(log_data)
+ # Log buffer should have 3 logs
+ self.assertEqual(log_data * 3, gl.log_buffer.getbuffer())
+ # Input buffer now empty
+ self.assertEqual(b'', gl.input_buffer.getbuffer())
+
+ def test_read_until(self) -> None:
+ """Test BytearrayLoop.read_until."""
+ log_data = b''.join([
+ _SAMPLE_ENCODED_LOGS[1],
+ _SAMPLE_ENCODED_LOGS[2],
+ ])
+ proto_data = b''.join([
+ _SAMPLE_PROTO_PACKETS[0],
+ _SAMPLE_PROTO_PACKETS[1],
+ ])
+
+ with self.subTest(
+ msg="Matching sub sequence exists; read until that."):
+ merged_data = log_data + proto_data
+ bl = BytearrayLoop(merged_data)
+ self.assertEqual(log_data, bl.read_until(BIN_LOG_SYNC_START_BYTES))
+ self.assertEqual(proto_data, bl.getbuffer())
+
+ with self.subTest(
+ msg="No matching sub sequence exists; read all data."):
+ bl = BytearrayLoop(log_data)
+ self.assertEqual(log_data, bl.read_until(BIN_LOG_SYNC_START_BYTES))
+ # Remaining buffer should be empty
+ self.assertEqual(b'', bl.getbuffer())
+
+ with self.subTest(
+ msg="Matching sub sequence at the start, read nothing."):
+ bl = BytearrayLoop(proto_data)
+ self.assertEqual(b'', bl.read_until(BIN_LOG_SYNC_START_BYTES))
+ # Remaining buffer should be full
+ self.assertEqual(proto_data, bl.getbuffer())
+
+ with self.subTest(msg="Subsequence is longer than the buffer."):
+ # Searching for a subsequece longer than the buffer should return an
+ # empty result.
+ too_short_buffer = BytearrayLoop(bytes.fromhex('abc123'))
+ self.assertEqual(
+ b'',
+ too_short_buffer.read_until(bytes.fromhex('abc123456789')))
+
+ def test_proto_packet_length(self) -> None:
+ """Test decode_one_varint on remaining protobuf lengths."""
+ for packet in _SAMPLE_PROTO_PACKETS:
+ bl = BytearrayLoop()
+
+ bl.write(packet)
+ self.assertEqual(packet, bl.getbuffer())
+
+ original_size = len(packet)
+ packet_size = original_size
+
+ self.assertEqual(BIN_LOG_SYNC_START_BYTES, bl.peek(5))
+ # Read magic bytes fixed32 tag + 4 bytes.
+ magic_start = bl.read(5)
+ self.assertEqual(BIN_LOG_SYNC_START_BYTES, magic_start)
+ packet_size -= 5
+
+ # Read the tag.
+ _tag = bl.read(1)
+ packet_size -= 1
+
+ # Read the varint with the remaining packet size.
+ original_size = len(bl.getbuffer())
+ decoded_remaining_size = decode_one_varint(bl)
+ # Subtract bytes used by the varint
+ packet_size -= original_size - len(bl.getbuffer())
+
+ self.assertEqual(decoded_remaining_size, packet_size)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/gonk_tools/binary_handler.py b/tools/gonk_tools/binary_handler.py
new file mode 100644
index 0000000..ead60b7
--- /dev/null
+++ b/tools/gonk_tools/binary_handler.py
@@ -0,0 +1,97 @@
+# 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.
+"""Binary log data functions."""
+
+import io
+import threading
+from typing import Union
+
+BIN_LOG_SYNC_START_STR = '0d99bbaa7d'
+BIN_LOG_SYNC_START_BYTES = bytes.fromhex(BIN_LOG_SYNC_START_STR)
+
+
+class BytearrayLoop:
+ """A bytearray reader writer.
+
+ Reads occur from the front of the buffer and remove data. Writes append to
+ the end of the buffer.
+ """
+ def __init__(self, data: bytes = b''):
+ self.buffer = bytearray(data)
+ self.lock = threading.Lock()
+
+ def read(self, size: int = -1) -> bytearray:
+ """Read bytes from the start of the buffer."""
+ data = b''
+ with self.lock:
+ if size < 0:
+ size = len(self.buffer)
+ data = self.buffer[:size]
+ # Delete the data we just fetched
+ self.buffer[:size] = b''
+ return data
+
+ def write(self, data: bytes) -> None:
+ """Write bytes to the end of the buffer."""
+ with self.lock:
+ self.buffer.extend(data)
+
+ def peek(self, size: int = 0) -> bytearray:
+ """Peek into bytes from the start."""
+ return self.buffer[:size]
+
+ def __len__(self) -> int:
+ return len(self.buffer)
+
+ def getbuffer(self) -> bytearray:
+ """Return the whole buffer."""
+ return self.buffer
+
+ def read_until(self, sub_sequece: bytes) -> bytearray:
+ """Pop all data from the buffer until a subsequence is found."""
+ # Return nothing if the buffer is smaller than the provided subsequence.
+ if len(sub_sequece) > len(self.buffer):
+ return bytearray()
+ position = self.buffer.find(sub_sequece)
+ # If no subsequece is found, read all data.
+ if position < 0:
+ return self.read()
+ return self.read(position)
+
+
+_RawIo = Union[io.RawIOBase, BytearrayLoop]
+
+
+def decode_one_varint(stream: _RawIo) -> int:
+ """Read a single varint from a byte stream."""
+ # Track the amount shifted.
+ shift = 0
+ # Resulting varint.
+ result = 0
+ while True:
+ # Get one byte
+ b = stream.read(1)
+ if not b:
+ break
+ # Read as an integer. Note: byte order is little endian but we are only
+ # reading one byte at a time.
+ i = int.from_bytes(b, byteorder='little')
+ # Shift first 7 bits from this byte into the result.
+ result |= (i & 0x7f) << shift
+ shift += 7
+ # Stop if the highest bit is not set.
+ if not i & 0x80:
+ break
+
+ return result
diff --git a/tools/gonk_tools/gonk_log_stream.py b/tools/gonk_tools/gonk_log_stream.py
new file mode 100644
index 0000000..6709037
--- /dev/null
+++ b/tools/gonk_tools/gonk_log_stream.py
@@ -0,0 +1,220 @@
+# 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.
+"""Gonk log stream handler."""
+
+from datetime import datetime
+import logging
+import time
+from typing import Optional
+
+from google.protobuf.message import DecodeError
+from pw_hdlc.decode import FrameDecoder
+from pw_log.log_decoder import LogStreamDecoder
+from pw_log.proto import log_pb2
+from pw_tokenizer import detokenize
+import pw_log.log_decoder
+
+from gonk_adc.adc_measurement_pb2 import FramedProto
+from gonk_tools.binary_handler import (
+ BIN_LOG_SYNC_START_BYTES,
+ BytearrayLoop,
+)
+
+_LOG = logging.getLogger('host')
+_DEVICE_LOG = logging.getLogger('gonk')
+
+
+def emit_python_log(log: pw_log.log_decoder.Log) -> None:
+ log.metadata_fields['module'] = log.module_name
+ log.metadata_fields['source_name'] = log.source_name
+ log.metadata_fields['timestamp'] = log.timestamp
+ log.metadata_fields['msg'] = log.message
+ log.metadata_fields['file'] = log.file_and_line
+
+ _DEVICE_LOG.log(
+ log.level,
+ '%s -- %s',
+ log.message,
+ log.file_and_line,
+ extra=dict(extra_metadata_fields=log.metadata_fields),
+ )
+
+
+class GonkLogStream:
+ """Handle incoming serial data from Gonk."""
+
+ # pylint: disable=too-many-instance-attributes
+ def __init__(
+ self,
+ detokenizer: Optional[detokenize.Detokenizer] = None,
+ adc_time_format: str = '%Y%m%d %H:%M:%S.%f',
+ ) -> None:
+ self.detokenizer = detokenizer
+ self.log_decoder: Optional[LogStreamDecoder] = None
+ if self.detokenizer:
+ self.log_decoder = LogStreamDecoder(
+ decoded_log_handler=emit_python_log, detokenizer=detokenizer)
+ self.input_buffer = BytearrayLoop()
+ self.log_buffer = BytearrayLoop()
+ self.frame_decoder = FrameDecoder()
+ self.adc_time_format = adc_time_format
+
+ self.remaining_proto_bytes: Optional[int] = None
+
+ self.proto_update_count: int = 0
+ self.proto_update_time = time.monotonic()
+
+ def _maybe_log_update_rate(self) -> None:
+ self.proto_update_count += 1
+ current_time = time.monotonic()
+ if current_time > self.proto_update_time + 1:
+ _LOG.info('ADC updates/second: %d', self.proto_update_count)
+ self.proto_update_count = 0
+ self.proto_update_time = current_time
+
+ def write(self, data: bytes) -> None:
+ """Write data and separate protobuf data from logs."""
+ self.input_buffer.write(data)
+
+ logdata = self.input_buffer.read_until(BIN_LOG_SYNC_START_BYTES)
+ if logdata:
+ self.log_buffer.write(logdata)
+
+ self._handle_log_data()
+ self._handle_proto_packets()
+
+ def _next_proto_packet_size(self) -> Optional[int]:
+ packet_size: Optional[int] = None
+
+ required_bytes = 8
+ # Expected bytes:
+ # 1: fixed32 tag
+ # 4: sync start value bytes
+ # 1: varint tag
+ # 2: varint value (one to two bytes)
+ head_bytes = self.input_buffer.peek(required_bytes)
+ if len(
+ head_bytes
+ ) < required_bytes or BIN_LOG_SYNC_START_BYTES not in head_bytes:
+ return packet_size
+
+ _sync_start_bytes = head_bytes[:5]
+ _varint_tag = head_bytes[5:6]
+ # Decode 1-2 varint bytes.
+ varint_byte1 = head_bytes[6:7]
+ varint_byte2 = head_bytes[7:8]
+ i = int.from_bytes(varint_byte1)
+ packet_size = i & 0x7f
+ if (i & 0x80) == 0x80:
+ # Decode the extra varint byte
+ packet_size |= (int.from_bytes(varint_byte2) & 0x7f) << 7
+ else:
+ # Extra varint byte value not used.
+ required_bytes -= 1
+
+ return required_bytes + packet_size
+
+ def _handle_log_data(self) -> None:
+ log_data = self.log_buffer.read()
+ for frame in self.frame_decoder.process(log_data):
+ if not frame.ok():
+ _LOG.warning(
+ 'Failed to decode frame: %s; discarded %d bytes',
+ frame,
+ len(frame.raw_encoded),
+ )
+ # Save this data back to the log_buffer
+ self.log_buffer.write(frame.raw_encoded)
+ continue
+
+ if not self.log_decoder:
+ _DEVICE_LOG.info('%s', frame)
+ continue
+
+ log_entry = log_pb2.LogEntry()
+ try:
+ log_entry.ParseFromString(frame.data)
+ except DecodeError:
+ # Try to detokenize the frame data and log the failure.
+ detokenized_text = ''
+ if self.detokenizer:
+ detokenized_text = str(
+ self.detokenizer.detokenize(frame.data))
+ if detokenized_text:
+ _LOG.warning('Failed to parse log proto "%s" %s',
+ detokenized_text, frame)
+ else:
+ _LOG.warning('Failed to parse log proto %s', frame)
+ continue
+
+ log = self.log_decoder.parse_log_entry_proto(log_entry)
+ emit_python_log(log)
+
+ def _handle_proto_packets(self) -> None:
+ # If no proto has been found, check for a new one.
+ if self.remaining_proto_bytes is None:
+ self.remaining_proto_bytes = self._next_proto_packet_size()
+
+ # If a proto was found and bytes are pending.
+ if (self.remaining_proto_bytes is not None
+ and self.remaining_proto_bytes > 0):
+
+ # If the remaining bytes are available in the buffer.
+ if self.remaining_proto_bytes == len(
+ self.input_buffer.peek(self.remaining_proto_bytes)):
+ # Pop the proto
+ proto_bytes = bytes(
+ self.input_buffer.read(self.remaining_proto_bytes))
+ # All bytes consumed
+ self.remaining_proto_bytes = None
+ self._parse_and_log_adc_proto(proto_bytes)
+ self._maybe_log_update_rate()
+
+ def _parse_and_log_adc_proto(self, proto_bytes: bytes) -> None:
+ """Parse an ADC proto message and log."""
+ framed_proto = FramedProto()
+
+ try:
+ framed_proto.ParseFromString(proto_bytes)
+ except DecodeError:
+ _LOG.error('ADC FramedProto.DecodeError: %s', proto_bytes.hex())
+ return
+
+ host_time = datetime.now().strftime(self.adc_time_format)
+ packet_size = len(proto_bytes)
+ delta_micros = framed_proto.payload.timestamp
+
+ 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)
+
+ _DEVICE_LOG.info(
+ 'host_time: %s size: %s delta_microseconds: %s '
+ 'vbus: %s vshunt: %s',
+ host_time,
+ str(packet_size),
+ str(delta_micros),
+ ','.join(str(value) for value in vshunt_values),
+ ','.join(str(value) for value in vbus_values),
+ extra=dict(
+ extra_metadata_fields={
+ 'host_time': host_time,
+ 'packet_size': packet_size,
+ 'delta_micros': delta_micros,
+ 'vbus_values': vbus_values,
+ 'vshunt_values': vshunt_values,
+ }),
+ )
diff --git a/tools/gonk_tools/write_fpga.py b/tools/gonk_tools/write_fpga.py
index 9699bcd..fd879f7 100644
--- a/tools/gonk_tools/write_fpga.py
+++ b/tools/gonk_tools/write_fpga.py
@@ -14,23 +14,34 @@
"""Write a binary over serial to provision the Gonk FPGA."""
import argparse
-from datetime import datetime
import importlib.resources
from itertools import islice
+import logging
import operator
from pathlib import Path
import sys
import time
-from typing import Optional
+from typing import Optional, Iterable
import serial
from serial import Serial
from serial.tools.list_ports import comports
from serial.tools.miniterm import Miniterm, Transform
-from google.protobuf.message import DecodeError
+from pw_cli import log as pw_cli_log
+from pw_console import python_logging
import pw_cli.color
-from gonk_adc.adc_measurement_pb2 import FramedProto
+from pw_tokenizer import (
+ database as pw_tokenizer_database,
+ detokenize,
+ tokens,
+)
+
+from gonk_tools.gonk_log_stream import GonkLogStream
+
+_ROOT_LOG = logging.getLogger()
+_LOG = logging.getLogger('host')
+_DEVICE_LOG = logging.getLogger('gonk')
_COLOR = pw_cli.color.colors()
@@ -49,7 +60,22 @@
def _parse_args():
- parser = argparse.ArgumentParser(description=__doc__)
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument(
+ '--databases',
+ metavar='elf_or_token_database',
+ action=pw_tokenizer_database.LoadTokenDatabases,
+ nargs='+',
+ help=('ELF or token database files from which to read strings and '
+ 'tokens. For ELF files, the tokenization domain to read from '
+ 'may specified after the path as #domain_name (e.g. '
+ 'foo.elf#TEST_DOMAIN). Unless specified, only the default '
+ 'domain ("") is read from ELF files; .* reads all domains. '
+ 'Globs are expanded to compatible database files.'),
+ )
parser.add_argument(
'-p',
'--port',
@@ -78,6 +104,49 @@
help=('FPGA Bitstream file. Can be a filesystem path or "DEFAULT" to '
'use a Gonk Python tools bundled binary file.'),
)
+ parser.add_argument(
+ '--logfile',
+ type=Path,
+ default=Path('gonk-logs.txt'),
+ help=('Default log file. This will contain host side '
+ 'log messages only unles the '
+ '--merge-device-and-host-logs argument is used.'),
+ )
+ parser.add_argument(
+ '--merge-device-and-host-logs',
+ action='store_true',
+ help=('Include device logs in the default --logfile.'
+ 'These are normally shown in a separate device '
+ 'only log file.'),
+ )
+ parser.add_argument(
+ '--log-colors',
+ action=argparse.BooleanOptionalAction,
+ help=('Colors in log messages.'),
+ )
+ parser.add_argument(
+ '--log-to-stderr',
+ action='store_true',
+ help=('Log to STDERR'),
+ )
+ parser.add_argument(
+ '--host-logfile',
+ type=Path,
+ help=('Additional host only log file. Normally all logs in the '
+ 'default logfile are host only.'),
+ )
+ parser.add_argument(
+ '--device-logfile',
+ type=Path,
+ default=Path('gonk-device-logs.txt'),
+ help='Device only log file.',
+ )
+ parser.add_argument(
+ '--json-logfile',
+ help='Device only JSON formatted log file.',
+ type=Path,
+ )
+
return parser.parse_args()
@@ -110,9 +179,6 @@
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 UnknownSerialDevice(Exception):
"""Exception raised when no device is specified."""
@@ -141,82 +207,32 @@
class HandleBinaryData(Transform):
"""Miniterm transform to handle incoming byte data."""
- def __init__(self) -> None:
- self.data = bytes()
- self.timestamp_prefix = ' delta_microseconds:'
- 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 __init__(self, gonk_log_stream: GonkLogStream) -> None:
+ self.gonk_log_stream = gonk_log_stream
- def _parse_proto(self, proto_bytes: bytes) -> str:
- # Parse the proto message.
- 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 = [
- # Host time
- datetime.now().strftime(self.time_format),
- # Update byte size
- f' size: {str(len(proto_bytes))} ',
- self.timestamp_prefix,
- # Delta microseconds
- str(framed_proto.payload.timestamp),
- # Vshunt values
- self.vshunt_prefix,
- ','.join(str(value) for value in vshunt_values),
- # Vbus values
- self.vbus_prefix,
- ','.join(str(value) for value in vbus_values),
- ]
- return ' '.join(output) + '\n'
- except DecodeError:
- # TODO(tonymd): Handle failed packets.
- return 'FramedProto.DecodeError\n'
-
- def rx(self, text, data=None):
+ def rx(self, text: str, data: Optional[bytes] = None) -> str:
"""Text received from the 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
+ if data:
+ self.gonk_log_stream.write(data)
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]
-
- return self._parse_proto(proto_bytes)
-
- def tx(self, text):
- """Text to be sent to the serial port."""
return text
- def echo(self, text):
+
+class DebugSerialIO(Transform):
+ """Print sent and received data to stderr."""
+ def rx(self, text: str) -> str:
+ """Text received from the serial port."""
+ sys.stderr.write('[Recv: {!r}] '.format(text))
+ sys.stderr.flush()
+ return text
+
+ def tx(self, text: str):
+ """Text to be sent to the serial port."""
+ sys.stderr.write(' [Send: {!r}] '.format(text))
+ sys.stderr.flush()
+ return text
+
+ def echo(self, text: str):
"""Text to be sent but displayed on console."""
return text
@@ -277,24 +293,105 @@
"""Write a series of bytes to serial."""
# Write out the bitstream in batches.
- print('Sending bitstream...')
+ _LOG.info('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}')
+ _LOG.info('Done sending bitstream. Wrote %d', written_bytes)
def main(
+ # pylint: disable=too-many-arguments
baudrate: int,
+ databases: Iterable,
bitstream_file: Optional[Path],
port: Optional[str] = None,
product: Optional[str] = None,
serial_number: Optional[str] = None,
+ logfile: Optional[str] = None,
+ host_logfile: Optional[str] = None,
+ device_logfile: Optional[str] = None,
+ json_logfile: Optional[str] = None,
+ log_colors: Optional[bool] = False,
+ merge_device_and_host_logs: bool = False,
+ verbose: bool = False,
+ log_to_stderr: bool = False,
) -> int:
"""Write a bitstream file over serial while monitoring output."""
+
+ if not logfile:
+ # Create a temp logfile to prevent logs from appearing over stdout. This
+ # would corrupt the prompt toolkit UI.
+ logfile = python_logging.create_temp_log_file()
+
+ if log_colors is None:
+ log_colors = True
+ colors = pw_cli.color.colors(log_colors)
+ log_level = logging.DEBUG if verbose else logging.INFO
+ logger_name_format = colors.cyan('%(name)s')
+ logger_message_format = f'[{logger_name_format}] %(levelname)s %(message)s'
+
+ pw_cli_log.install(
+ level=log_level,
+ use_color=log_colors,
+ hide_timestamp=False,
+ log_file=logfile,
+ message_format=logger_message_format,
+ )
+
+ if device_logfile:
+ pw_cli_log.install(
+ level=log_level,
+ use_color=log_colors,
+ hide_timestamp=False,
+ log_file=device_logfile,
+ logger=_DEVICE_LOG,
+ message_format=logger_message_format,
+ )
+ if host_logfile:
+ pw_cli_log.install(
+ level=log_level,
+ use_color=log_colors,
+ hide_timestamp=False,
+ log_file=host_logfile,
+ logger=_ROOT_LOG,
+ message_format=logger_message_format,
+ )
+
+ # By default don't send device logs to the root logger.
+ _DEVICE_LOG.propagate = False
+ if merge_device_and_host_logs:
+ # Add device logs to the default logfile.
+ pw_cli_log.install(
+ level=log_level,
+ use_color=log_colors,
+ hide_timestamp=False,
+ log_file=logfile,
+ logger=_DEVICE_LOG,
+ message_format=logger_message_format,
+ )
+
+ if json_logfile:
+ json_filehandler = logging.FileHandler(json_logfile, encoding='utf-8')
+ json_filehandler.setLevel(log_level)
+ json_filehandler.setFormatter(python_logging.JsonLogFormatter())
+ _DEVICE_LOG.addHandler(json_filehandler)
+
+ if log_to_stderr:
+ pw_cli_log.install(
+ level=log_level,
+ use_color=log_colors,
+ hide_timestamp=False,
+ message_format=logger_message_format,
+ )
+
+ _LOG.setLevel(log_level)
+ _DEVICE_LOG.setLevel(log_level)
+ _ROOT_LOG.setLevel(log_level)
+
# Init serial port.
if port is None:
port = get_serial_port(product=product, serial_number=serial_number)
@@ -305,19 +402,25 @@
serial_instance = Serial(port=port, baudrate=baudrate, timeout=0.1)
+ detokenizer = detokenize.Detokenizer(tokens.Database.merged(*databases),
+ show_errors=True)
+
# Use pyserial miniterm to monitor recieved data.
miniterm = MinitermBinary(
serial_instance,
- echo=False,
- eol='crlf',
+ echo=True,
+ eol='lf',
filters=(),
)
+
# Use Ctrl-C as the exit character. (Miniterm default is Ctrl-])
miniterm.exit_character = chr(0x03)
miniterm.set_rx_encoding('utf-8', errors='backslashreplace')
miniterm.set_tx_encoding('utf-8')
- miniterm.rx_transformations.append(HandleBinaryData())
+ gonk_log_stream = GonkLogStream(detokenizer)
+ miniterm.rx_transformations.append(HandleBinaryData(gonk_log_stream))
+ miniterm.tx_transformations.append(DebugSerialIO())
# Start monitoring serial data.
miniterm.start()