1
0
forked from M-Labs/artiq

Compare commits

...

17 Commits

Author SHA1 Message Date
Pachigulla Ramtej
1f71cb9538 Merge remote-tracking branch 'upstream/nac3' into nac3 2025-04-15 11:45:21 +08:00
5392ba3812 flake: update dependencies 2025-04-11 11:10:46 +08:00
611a4bcaab Merge branch 'master' into nac3 2025-04-11 11:02:05 +08:00
0389be14fe i2c: fix issues with building 2025-04-10 11:00:21 +08:00
27241cb2c3 fix LLVM string codegen test
See #2713 for the main changes.
2025-04-02 13:59:54 +08:00
a4ad97a3fd FFI: pass string parameters directly 2025-04-01 16:32:28 +08:00
4ddad5fd17 i2c: stop the bus after received NACK 2025-03-28 14:41:57 +08:00
David Nadlinger
d78ebb6bbb dashboard: Dark-mode support for Log pane
Integrates much more naturally with dark mode (e.g. the default dark
system theme on Windows 11) by continuing to use bright text on dark
backgrounds in that case.
2025-03-26 10:03:14 +08:00
464befba63 cxp_grabber pixtracker: set y to y_size after eof 2025-03-25 12:52:29 +08:00
1075e89514 cxp_grabber driver: expose xml & pixel read api 2025-03-24 14:07:21 +08:00
342fa5aaf6 cxp_grabber driver doc: add roiviewer height limit 2025-03-20 15:59:18 +08:00
fc5bc1bb5c cxp_grabber driver: fix roiviewer coordinate order 2025-03-20 15:59:18 +08:00
9e655332e3 cxp test: fix pixel packet generator bit order 2025-03-18 13:00:39 +08:00
b3678e8bfb cxp_grabber: fix pixel unpacker bit order 2025-03-18 13:00:39 +08:00
Yichao Yu
dcc0f8d579 Make master worker ipc threadsafe on the worker side
This allows the other threads to, e.g., access dataset when the main threads
runs the kernels.

Fix #2701
2025-03-17 23:06:17 +08:00
afbd83799c ksupport: invalidate I-cache after rebind
Rebinding modifies instructions directly, if binary is linked using nac3ld
2025-03-17 21:58:16 +08:00
641d4cc362 cxp_grabber driver: add cxp grabber exceptions
driver: add outofsync & timeout exceptions instead of importing
2025-03-15 10:57:25 +08:00
10 changed files with 207 additions and 125 deletions

View File

@ -19,6 +19,7 @@ ARTIQ-9 (Unreleased)
for the AD9834 DDS, tested with the ZonRi Technology Co., Ltd. AD9834-Module. for the AD9834 DDS, tested with the ZonRi Technology Co., Ltd. AD9834-Module.
* Dashboard: * Dashboard:
- Experiment windows can have different colors, selected by the user. - Experiment windows can have different colors, selected by the user.
- The Log pane now adapts to dark system color themes.
- Schedule display columns can now be reordered and shown/hidden using the table - Schedule display columns can now be reordered and shown/hidden using the table
header context menu. header context menu.
- State files are now automatically backed up upon successful loading. - State files are now automatically backed up upon successful loading.

View File

@ -1,13 +1,24 @@
from numpy import array, int32, int64, uint8, uint16, iinfo from numpy import array, int32, int64, ndarray
from artiq.language.core import syscall, kernel from artiq.language.core import syscall, kernel
from artiq.language.types import TInt32, TNone, TList from artiq.language.types import TInt32, TNone, TList
from artiq.coredevice.rtio import rtio_output, rtio_input_timestamped_data from artiq.coredevice.rtio import rtio_output, rtio_input_timestamped_data
from artiq.coredevice.grabber import OutOfSyncException, GrabberTimeoutException
from artiq.experiment import * from artiq.experiment import *
class OutOfSyncException(Exception):
"""Raised when an incorrect number of ROI engine outputs has been
retrieved from the RTIO input FIFO."""
pass
class CXPGrabberTimeoutException(Exception):
"""Raised when a timeout occurs while attempting to read CoaXPress Grabber RTIO input events."""
pass
@syscall(flags={"nounwind"}) @syscall(flags={"nounwind"})
def cxp_download_xml_file(buffer: TList(TInt32)) -> TInt32: def cxp_download_xml_file(buffer: TList(TInt32)) -> TInt32:
raise NotImplementedError("syscall not simulated") raise NotImplementedError("syscall not simulated")
@ -24,7 +35,7 @@ def cxp_write32(addr: TInt32, val: TInt32) -> TNone:
@syscall(flags={"nounwind"}) @syscall(flags={"nounwind"})
def cxp_start_roi_viewer(x0: TInt32, x1: TInt32, y0: TInt32, y1: TInt32) -> TNone: def cxp_start_roi_viewer(x0: TInt32, y0: TInt32, x1: TInt32, y1: TInt32) -> TNone:
raise NotImplementedError("syscall not simulated") raise NotImplementedError("syscall not simulated")
@ -35,6 +46,79 @@ def cxp_download_roi_viewer_frame(
raise NotImplementedError("syscall not simulated") raise NotImplementedError("syscall not simulated")
def write_file(data, file_path):
"""
Write big-endian encoded data to PC
:param data: a list of 32-bit integers
:param file_path: a relative path on PC
**Examples:**
To download the XML file to PC: ::
# Prepare a big enough buffer
buffer = [0] * 25600
# Read the XML file and write it to PC
cxp_grabber.read_local_xml(buffer)
write_file(buffer, "camera_setting.xml")
"""
array(data, dtype=">i").tofile(file_path)
def write_pgm(frame, file_path, pixel_width):
"""
Write the frame as PGM file to PC.
:param frame: a 2D array of 32-bit integers
:param file_path: a relative path on PC
:param pixel_width: bit depth that the PGM will use (8 or 16)
**Examples:**
To capture a 32x64 frame and write it as a 8-bit PGM file to PC: ::
# Prepare a 32x64 2D array
frame = numpy.array([[0] * 32] * 64)
# Setup the camera to use LinkTriger0 and start acquisition
# (Read the camera setting XML file for details)
cxp_grabber.write32(TRIG_SETTING_ADDR, 0)
...
# Setup ROI viewer coordinate and start the viewer capture
cxp_grabber.start_roi_viewer(0, 0, 32, 64)
# Send LinkTrigger0
cxp_grabber.send_cxp_linktrigger(0)
# Read the frame from ROI viewer and write it as a 8-bit PGM image to PC
cxp_grabber.read_roi_viewer_frame(frame)
write_pgm(frame, "frame.pgm", 8)
"""
if not isinstance(frame, ndarray):
raise ValueError("Frame must be a numpy array")
if pixel_width == 8:
frame = frame.astype("u1")
elif pixel_width == 16:
# PGM use big-endian
frame = frame.astype(">u2")
else:
raise ValueError("PGM file format only supports 8-bit or 16-bit per pixel")
# Save as PGM binary variant
# https://en.wikipedia.org/wiki/Netpbm#Description
with open(file_path, "wb") as file:
max_value = (2**pixel_width) - 1
width, height = len(frame[0]), len(frame)
file.write(f"P5\n{width} {height}\n{max_value}\n".encode("ASCII"))
file.write(frame.tobytes())
class CXPGrabber: class CXPGrabber:
"""Driver for the CoaXPress Grabber camera interface.""" """Driver for the CoaXPress Grabber camera interface."""
@ -153,7 +237,7 @@ class CXPGrabber:
this call or the next. this call or the next.
If the timeout is reached before data is available, the exception If the timeout is reached before data is available, the exception
:exc:`artiq.coredevice.grabber.GrabberTimeoutException` is raised. :exc:`CXPGrabberTimeoutException` is raised.
:param timeout_mu: Timestamp at which a timeout will occur. Set to -1 :param timeout_mu: Timestamp at which a timeout will occur. Set to -1
(default) to disable timeout. (default) to disable timeout.
@ -162,7 +246,9 @@ class CXPGrabber:
timeout_mu, self.roi_gating_ch timeout_mu, self.roi_gating_ch
) )
if timestamp == -1: if timestamp == -1:
raise GrabberTimeoutException("Timeout before Grabber frame available") raise CXPGrabberTimeoutException(
"Timeout before CoaXPress Grabber frame available"
)
if sentinel != self.sentinel: if sentinel != self.sentinel:
raise OutOfSyncException raise OutOfSyncException
@ -173,7 +259,7 @@ class CXPGrabber:
if roi_output == self.sentinel: if roi_output == self.sentinel:
raise OutOfSyncException raise OutOfSyncException
if timestamp == -1: if timestamp == -1:
raise GrabberTimeoutException( raise CXPGrabberTimeoutException(
"Timeout retrieving ROIs (attempting to read more ROIs than enabled?)" "Timeout retrieving ROIs (attempting to read more ROIs than enabled?)"
) )
data[i] = roi_output data[i] = roi_output
@ -203,97 +289,52 @@ class CXPGrabber:
cxp_write32(address, value) cxp_write32(address, value)
@kernel @kernel
def download_local_xml(self, file_path, buffer_size=102400): def read_local_xml(self, buffer):
""" """
Downloads the XML setting file to PC from the camera if available. Read the XML setting file from the camera if available.
Data will be in 32-bit big-endian encoding.
The file format may be .zip or .xml depending on the camera model. The file format may be .zip or .xml depending on the camera model.
.. warning:: This is NOT a real-time operation. .. warning:: This is NOT a real-time operation.
:param file_path: a relative path on PC :param buffer: list to be filled
:param buffer_size: size of read buffer expressed in bytes; must be a multiple of 4 :returns: number of 32-bit words read
""" """
buffer = [0] * (buffer_size // 4) return cxp_download_xml_file(buffer)
size_read = cxp_download_xml_file(buffer)
self._write_file(buffer[:size_read], file_path)
@rpc
def _write_file(self, data, file_path):
"""
Write big endian encoded data into a file
:param data: a list of 32-bit integers
:param file_path: a relative path on PC
"""
byte_arr = bytearray()
for d in data:
byte_arr += d.to_bytes(4, "big", signed=True)
with open(file_path, "wb") as binary_file:
binary_file.write(byte_arr)
@kernel @kernel
def start_roi_viewer(self, x0, x1, y0, y1): def start_roi_viewer(self, x0, y0, x1, y1):
""" """
Defines the coordinates of ROI viewer and start the capture. Defines the coordinates of ROI viewer and start the capture.
Unlike :exc:`setup_roi`, ROI viewer has a maximum size limit of 4096 pixels. Unlike :exc:`setup_roi`, ROI viewer has a maximum height limit of 1024 and total size limit of 4096 pixels.
.. warning:: This is NOT a real-time operation. .. warning:: This is NOT a real-time operation.
""" """
cxp_start_roi_viewer(x0, x1, y0, y1) cxp_start_roi_viewer(x0, y0, x1, y1)
@kernel @kernel
def download_roi_viewer_frame(self, file_path): def read_roi_viewer_frame(self, frame):
""" """
Downloads the ROI viewer frame as a PGM file to PC. Read the ROI viewer frame.
The user must :exc:`start_roi_viewer` and trigger the camera before the frame is avaiable. The user must :exc:`start_roi_viewer` and trigger the camera before the frame is available.
.. warning:: This is NOT a real-time operation. .. warning:: This is NOT a real-time operation.
:param file_path: a relative path on PC :param frame: a 2D array of 32-bit integers
:returns: the frame bit depth
""" """
buffer = [0] * 1024 buffer = [0] * 1024
width, height, pixel_width = cxp_download_roi_viewer_frame(buffer) width, height, pixel_width = cxp_download_roi_viewer_frame(buffer)
self._write_pgm(buffer, width, height, pixel_width, file_path) if height != len(frame) or width != len(frame[0]):
raise ValueError(
"The frame matrix size is not the same as ROI viewer frame size"
)
@rpc for y in range(height):
def _write_pgm(self, data, width, height, pixel_width, file_path): offset = (((width + 3) & (~3)) // 4) * y
""" for x in range(width):
Write pixel data into a PGM file. # each buffer element holds 4 pixels
frame[y][x] = (buffer[offset + (x // 4)] >> (16 * (x % 4))) & 0xFFFF
:param data: a list of 64-bit integers return pixel_width
:param file_path: a relative path on PC
"""
if ".pgm" not in file_path.lower():
raise ValueError("The file extension must be .pgm")
pixels = []
width_aligned = (width + 3) & (~3)
for d in data[: width_aligned * height // 4]:
pixels += [
d & 0xFFFF,
(d >> 16) & 0xFFFF,
(d >> 32) & 0xFFFF,
(d >> 48) & 0xFFFF,
]
if pixel_width == 8:
dtype = uint8
else:
dtype = uint16
# pad to 16-bit for compatibility, as most software can only read 8, 16-bit PGM
pixels = [p << (16 - pixel_width) for p in pixels]
# trim the frame if the width is not multiple of 4
frame = array([pixels], dtype).reshape((height, width_aligned))[:, :width]
# save as PGM binary variant
# https://en.wikipedia.org/wiki/Netpbm#Description
with open(file_path, "wb") as file:
file.write(f"P5\n{width} {height}\n{iinfo(dtype).max}\n".encode("ASCII"))
if dtype == uint8:
file.write(frame.tobytes())
else:
# PGM use big endian
file.write(frame.astype(">u2").tobytes())

View File

@ -223,10 +223,11 @@ mod imp {
// mask in format of 1 << channel (or 0 for disabling output) // mask in format of 1 << channel (or 0 for disabling output)
// PCA9548 support only for now // PCA9548 support only for now
start(busno)?; start(busno)?;
write(busno, address << 1)?; let write_result = write(busno, address << 1)
write(busno, mask)?; .and_then( |_| write(busno, mask) );
stop(busno)?; let stop_result = stop(busno);
Ok(())
write_result.and(stop_result)
} }
} }

View File

@ -40,19 +40,21 @@ impl EEPROM {
self.select()?; self.select()?;
i2c::start(self.busno)?; i2c::start(self.busno)?;
i2c::write(self.busno, self.address)?; let read_result = i2c::write(self.busno, self.address)
i2c::write(self.busno, addr)?; .and_then( |_| i2c::write(self.busno, addr))
.and_then( |_| i2c::restart(self.busno))
.and_then( |_| i2c::write(self.busno, self.address | 1))
.and_then( |_| {
let buf_len = buf.len();
for (i, byte) in buf.iter_mut().enumerate() {
*byte = i2c::read(self.busno, i < buf_len - 1)?;
}
Ok(())
});
i2c::restart(self.busno)?; let stop_result = i2c::stop(self.busno);
i2c::write(self.busno, self.address | 1)?;
let buf_len = buf.len();
for (i, byte) in buf.iter_mut().enumerate() {
*byte = i2c::read(self.busno, i < buf_len - 1)?;
}
i2c::stop(self.busno)?; read_result.and(stop_result)
Ok(())
} }
/// > The 24AA02XEXX is programmed at the factory with a /// > The 24AA02XEXX is programmed at the factory with a

View File

@ -151,11 +151,12 @@ impl IoExpander {
fn write(&self, addr: u8, value: u8) -> Result<(), i2c::Error> { fn write(&self, addr: u8, value: u8) -> Result<(), i2c::Error> {
i2c::start(self.busno)?; i2c::start(self.busno)?;
i2c::write(self.busno, self.address)?; let write_result = i2c::write(self.busno, self.address)
i2c::write(self.busno, addr)?; .and_then( |_| i2c::write(self.busno, addr))
i2c::write(self.busno, value)?; .and_then( |_| i2c::write(self.busno, value));
i2c::stop(self.busno)?;
Ok(()) let stop_result = i2c::stop(self.busno);
write_result.and(stop_result)
} }
fn check_ack(&self) -> Result<bool, i2c::Error> { fn check_ack(&self) -> Result<bool, i2c::Error> {

View File

@ -303,8 +303,9 @@ class PixelUnpacker(Module):
sink_cases = {} sink_cases = {}
for i in range(ring_buf_size//sink_dw): for i in range(ring_buf_size//sink_dw):
byte = [self.sink.data[i * 8 : (i + 1) * 8] for i in range(sink_dw // 8)]
sink_cases[i] = [ sink_cases[i] = [
ring_buf[sink_dw*i:sink_dw*(i+1)].eq(self.sink.data), ring_buf[sink_dw*i:sink_dw*(i+1)].eq(Cat([b[::-1] for b in byte])),
] ]
self.sync += If(self.sink.stb, Case(w_cnt, sink_cases)) self.sync += If(self.sink.stb, Case(w_cnt, sink_cases))
@ -314,7 +315,7 @@ class PixelUnpacker(Module):
for j in range(4): for j in range(4):
source_cases[i].append( source_cases[i].append(
self.source.data[max_pixel_width * j : max_pixel_width * (j + 1)].eq( self.source.data[max_pixel_width * j : max_pixel_width * (j + 1)].eq(
ring_buf[(source_dw * i) + (size * j) : (source_dw * i) + (size * (j + 1))] ring_buf[(source_dw * i) + (size * j) : (source_dw * i) + (size * (j + 1))][::-1]
) )
) )
@ -403,7 +404,10 @@ class PixelCoordinateTracker(Module):
pix.x.eq(x_r), pix.x.eq(x_r),
pix.y.eq(y_r), pix.y.eq(y_r),
pix.gray.eq(self.sink.data[max_pixel_width*i:max_pixel_width*(i+1)]), pix.gray.eq(self.sink.data[max_pixel_width*i:max_pixel_width*(i+1)]),
) ),
If(pix.eof,
pix.y.eq(self.y_size),
),
] ]

View File

@ -7,6 +7,11 @@ from collections import namedtuple
_WORDLAYOUT = namedtuple("WordLayout", ["data", "k", "stb", "eop"]) _WORDLAYOUT = namedtuple("WordLayout", ["data", "k", "stb", "eop"])
def _switch_bit_order(s, width):
bits = "{:0{width}b}".format(s, width=width)
return int(bits[::-1], 2)
def MonoPixelPacketGenerator( def MonoPixelPacketGenerator(
x_size, x_size,
y_size, y_size,
@ -21,7 +26,7 @@ def MonoPixelPacketGenerator(
for x in range(x_size): for x in range(x_size):
# full white pixel # full white pixel
gray = (2**pixel_width) - 1 gray = (2**pixel_width) - 1
packed += gray << x * pixel_width packed += _switch_bit_order(gray, pixel_width) << x * pixel_width
# Line marker # Line marker
packet += [ packet += [
@ -41,11 +46,13 @@ def MonoPixelPacketGenerator(
for i in range(words_per_image_line): for i in range(words_per_image_line):
serialized = (packed & (0xFFFF_FFFF << i * word_width)) >> i * word_width serialized = (packed & (0xFFFF_FFFF << i * word_width)) >> i * word_width
word = []
for j in range(4):
word += [C(_switch_bit_order((serialized >> 8 * j) & 0xFF, 8), 8)]
eop = 1 if ((i == words_per_image_line - 1) and with_eol_marked) else 0 eop = 1 if ((i == words_per_image_line - 1) and with_eol_marked) else 0
packet.append( packet.append(
_WORDLAYOUT( _WORDLAYOUT(data=Cat(word), k=Replicate(0, 4), stb=1, eop=eop),
data=C(serialized, word_width), k=Replicate(0, 4), stb=1, eop=eop
),
) )
return packet return packet

View File

@ -43,7 +43,7 @@ class _LogFilterProxyModel(QtCore.QSortFilterProxyModel):
class _Model(QtCore.QAbstractItemModel): class _Model(QtCore.QAbstractItemModel):
def __init__(self): def __init__(self, palette):
QtCore.QAbstractTableModel.__init__(self) QtCore.QAbstractTableModel.__init__(self)
self.headers = ["Source", "Message"] self.headers = ["Source", "Message"]
@ -58,11 +58,16 @@ class _Model(QtCore.QAbstractItemModel):
self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.SystemFont.FixedFont) self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.SystemFont.FixedFont)
self.white = QtGui.QBrush(QtGui.QColor(255, 255, 255)) self.default_bg = palette.base()
self.black = QtGui.QBrush(QtGui.QColor(0, 0, 0)) self.default_fg = palette.text()
self.debug_fg = QtGui.QBrush(QtGui.QColor(55, 55, 55)) self.debug_fg = palette.placeholderText()
self.warning_bg = QtGui.QBrush(QtGui.QColor(255, 255, 180)) is_dark_mode = self.default_bg.color().lightness() < self.default_fg.color().lightness()
self.error_bg = QtGui.QBrush(QtGui.QColor(255, 150, 150)) if is_dark_mode:
self.warning_bg = QtGui.QBrush(QtGui.QColor(90, 74, 0))
self.error_bg = QtGui.QBrush(QtGui.QColor(98, 24, 24))
else:
self.warning_bg = QtGui.QBrush(QtGui.QColor(255, 255, 180))
self.error_bg = QtGui.QBrush(QtGui.QColor(255, 150, 150))
def headerData(self, col, orientation, role): def headerData(self, col, orientation, role):
if (orientation == QtCore.Qt.Orientation.Horizontal if (orientation == QtCore.Qt.Orientation.Horizontal
@ -163,13 +168,13 @@ class _Model(QtCore.QAbstractItemModel):
elif level >= logging.WARNING: elif level >= logging.WARNING:
return self.warning_bg return self.warning_bg
else: else:
return self.white return self.default_bg
elif role == QtCore.Qt.ItemDataRole.ForegroundRole: elif role == QtCore.Qt.ItemDataRole.ForegroundRole:
level = self.entries[msgnum][0] level = self.entries[msgnum][0]
if level <= logging.DEBUG: if level <= logging.DEBUG:
return self.debug_fg return self.debug_fg
else: else:
return self.black return self.default_fg
elif role == QtCore.Qt.ItemDataRole.DisplayRole: elif role == QtCore.Qt.ItemDataRole.DisplayRole:
v = self.entries[msgnum] v = self.entries[msgnum]
column = index.column() column = index.column()
@ -265,7 +270,7 @@ class LogDock(QDockWidgetCloseDetect):
cw = QtGui.QFontMetrics(self.font()).averageCharWidth() cw = QtGui.QFontMetrics(self.font()).averageCharWidth()
self.log.header().resizeSection(0, 26*cw) self.log.header().resizeSection(0, 26*cw)
self.model = _Model() self.model = _Model(self.palette())
self.proxy_model = _LogFilterProxyModel() self.proxy_model = _LogFilterProxyModel()
self.proxy_model.setSourceModel(self.model) self.proxy_model.setSourceModel(self.model)
self.log.setModel(self.proxy_model) self.log.setModel(self.proxy_model)

View File

@ -15,6 +15,7 @@ import traceback
from collections import OrderedDict from collections import OrderedDict
import importlib.util import importlib.util
import linecache import linecache
import threading
import h5py import h5py
@ -38,23 +39,42 @@ from artiq import __version__ as artiq_version
ipc = None ipc = None
ipc_lock = threading.Lock()
def get_object(): def get_object():
line = ipc.readline().decode() ipc_lock.acquire()
return pyon.decode(line) try:
line = ipc.readline()
finally:
ipc_lock.release()
return pyon.decode(line.decode())
def put_object(obj): def put_object(obj):
ds = pyon.encode(obj) ds = (pyon.encode(obj) + "\n").encode()
ipc.write((ds + "\n").encode()) ipc_lock.acquire()
try:
ipc.write(ds)
finally:
ipc_lock.release()
def put_and_get_object(obj):
ds = (pyon.encode(obj) + "\n").encode()
ipc_lock.acquire()
try:
ipc.write(ds)
line = ipc.readline()
finally:
ipc_lock.release()
return pyon.decode(line.decode())
def make_parent_action(action): def make_parent_action(action):
def parent_action(*args, **kwargs): def parent_action(*args, **kwargs):
request = {"action": action, "args": args, "kwargs": kwargs} request = {"action": action, "args": args, "kwargs": kwargs}
put_object(request) reply = put_and_get_object(request)
reply = get_object()
if "action" in reply: if "action" in reply:
if reply["action"] == "terminate": if reply["action"] == "terminate":
sys.exit() sys.exit()

14
flake.lock generated
View File

@ -48,11 +48,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1743494143, "lastModified": 1744341011,
"narHash": "sha256-OxeNED91hCgVsbgwRUpmP5BJ4dtilMxF2otGsQ+UBaQ=", "narHash": "sha256-sl8DJxAtqdLUxztNcgUWd9Xp1q271BLc/MfXmJlLkck=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "e4f6fbeeebd8d888f2a12d5be027c9394a37ad89", "rev": "800edf35db6ddb05b1f89d13b422b78d12ee024c",
"revCount": 1583, "revCount": 1588,
"type": "git", "type": "git",
"url": "https://git.m-labs.hk/m-labs/nac3.git" "url": "https://git.m-labs.hk/m-labs/nac3.git"
}, },
@ -63,11 +63,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1741851582, "lastModified": 1744098102,
"narHash": "sha256-cPfs8qMccim2RBgtKGF+x9IBCduRvd/N5F4nYpU0TVE=", "narHash": "sha256-tzCdyIJj9AjysC3OuKA+tMD/kDEDAF9mICPDU7ix0JA=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6607cf789e541e7873d40d3a8f7815ea92204f32", "rev": "c8cd81426f45942bb2906d5ed2fe21d2f19d95b7",
"type": "github" "type": "github"
}, },
"original": { "original": {