forked from M-Labs/artiq
Compare commits
17 Commits
180a0fface
...
1f71cb9538
Author | SHA1 | Date | |
---|---|---|---|
|
1f71cb9538 | ||
5392ba3812 | |||
611a4bcaab | |||
0389be14fe | |||
27241cb2c3 | |||
a4ad97a3fd | |||
4ddad5fd17 | |||
|
d78ebb6bbb | ||
464befba63 | |||
1075e89514 | |||
342fa5aaf6 | |||
fc5bc1bb5c | |||
9e655332e3 | |||
b3678e8bfb | |||
|
dcc0f8d579 | ||
afbd83799c | |||
641d4cc362 |
@ -19,6 +19,7 @@ ARTIQ-9 (Unreleased)
|
||||
for the AD9834 DDS, tested with the ZonRi Technology Co., Ltd. AD9834-Module.
|
||||
* Dashboard:
|
||||
- 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
|
||||
header context menu.
|
||||
- State files are now automatically backed up upon successful loading.
|
||||
|
@ -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.types import TInt32, TNone, TList
|
||||
from artiq.coredevice.rtio import rtio_output, rtio_input_timestamped_data
|
||||
from artiq.coredevice.grabber import OutOfSyncException, GrabberTimeoutException
|
||||
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"})
|
||||
def cxp_download_xml_file(buffer: TList(TInt32)) -> TInt32:
|
||||
raise NotImplementedError("syscall not simulated")
|
||||
@ -24,7 +35,7 @@ def cxp_write32(addr: TInt32, val: TInt32) -> TNone:
|
||||
|
||||
|
||||
@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")
|
||||
|
||||
|
||||
@ -35,6 +46,79 @@ def cxp_download_roi_viewer_frame(
|
||||
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:
|
||||
"""Driver for the CoaXPress Grabber camera interface."""
|
||||
|
||||
@ -153,7 +237,7 @@ class CXPGrabber:
|
||||
this call or the next.
|
||||
|
||||
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
|
||||
(default) to disable timeout.
|
||||
@ -162,7 +246,9 @@ class CXPGrabber:
|
||||
timeout_mu, self.roi_gating_ch
|
||||
)
|
||||
if timestamp == -1:
|
||||
raise GrabberTimeoutException("Timeout before Grabber frame available")
|
||||
raise CXPGrabberTimeoutException(
|
||||
"Timeout before CoaXPress Grabber frame available"
|
||||
)
|
||||
if sentinel != self.sentinel:
|
||||
raise OutOfSyncException
|
||||
|
||||
@ -173,7 +259,7 @@ class CXPGrabber:
|
||||
if roi_output == self.sentinel:
|
||||
raise OutOfSyncException
|
||||
if timestamp == -1:
|
||||
raise GrabberTimeoutException(
|
||||
raise CXPGrabberTimeoutException(
|
||||
"Timeout retrieving ROIs (attempting to read more ROIs than enabled?)"
|
||||
)
|
||||
data[i] = roi_output
|
||||
@ -203,97 +289,52 @@ class CXPGrabber:
|
||||
cxp_write32(address, value)
|
||||
|
||||
@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.
|
||||
|
||||
.. warning:: This is NOT a real-time operation.
|
||||
|
||||
:param file_path: a relative path on PC
|
||||
:param buffer_size: size of read buffer expressed in bytes; must be a multiple of 4
|
||||
:param buffer: list to be filled
|
||||
:returns: number of 32-bit words read
|
||||
"""
|
||||
buffer = [0] * (buffer_size // 4)
|
||||
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)
|
||||
return cxp_download_xml_file(buffer)
|
||||
|
||||
@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.
|
||||
|
||||
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.
|
||||
"""
|
||||
cxp_start_roi_viewer(x0, x1, y0, y1)
|
||||
cxp_start_roi_viewer(x0, y0, x1, y1)
|
||||
|
||||
@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.
|
||||
|
||||
: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
|
||||
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
|
||||
def _write_pgm(self, data, width, height, pixel_width, file_path):
|
||||
"""
|
||||
Write pixel data into a PGM file.
|
||||
|
||||
:param data: a list of 64-bit integers
|
||||
: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())
|
||||
for y in range(height):
|
||||
offset = (((width + 3) & (~3)) // 4) * y
|
||||
for x in range(width):
|
||||
# each buffer element holds 4 pixels
|
||||
frame[y][x] = (buffer[offset + (x // 4)] >> (16 * (x % 4))) & 0xFFFF
|
||||
return pixel_width
|
||||
|
@ -223,10 +223,11 @@ mod imp {
|
||||
// mask in format of 1 << channel (or 0 for disabling output)
|
||||
// PCA9548 support only for now
|
||||
start(busno)?;
|
||||
write(busno, address << 1)?;
|
||||
write(busno, mask)?;
|
||||
stop(busno)?;
|
||||
Ok(())
|
||||
let write_result = write(busno, address << 1)
|
||||
.and_then( |_| write(busno, mask) );
|
||||
let stop_result = stop(busno);
|
||||
|
||||
write_result.and(stop_result)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -40,19 +40,21 @@ impl EEPROM {
|
||||
self.select()?;
|
||||
|
||||
i2c::start(self.busno)?;
|
||||
i2c::write(self.busno, self.address)?;
|
||||
i2c::write(self.busno, addr)?;
|
||||
let read_result = i2c::write(self.busno, self.address)
|
||||
.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)?;
|
||||
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)?;
|
||||
}
|
||||
let stop_result = i2c::stop(self.busno);
|
||||
|
||||
i2c::stop(self.busno)?;
|
||||
|
||||
Ok(())
|
||||
read_result.and(stop_result)
|
||||
}
|
||||
|
||||
/// > The 24AA02XEXX is programmed at the factory with a
|
||||
|
@ -151,11 +151,12 @@ impl IoExpander {
|
||||
|
||||
fn write(&self, addr: u8, value: u8) -> Result<(), i2c::Error> {
|
||||
i2c::start(self.busno)?;
|
||||
i2c::write(self.busno, self.address)?;
|
||||
i2c::write(self.busno, addr)?;
|
||||
i2c::write(self.busno, value)?;
|
||||
i2c::stop(self.busno)?;
|
||||
Ok(())
|
||||
let write_result = i2c::write(self.busno, self.address)
|
||||
.and_then( |_| i2c::write(self.busno, addr))
|
||||
.and_then( |_| i2c::write(self.busno, value));
|
||||
|
||||
let stop_result = i2c::stop(self.busno);
|
||||
write_result.and(stop_result)
|
||||
}
|
||||
|
||||
fn check_ack(&self) -> Result<bool, i2c::Error> {
|
||||
|
@ -303,8 +303,9 @@ class PixelUnpacker(Module):
|
||||
|
||||
sink_cases = {}
|
||||
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] = [
|
||||
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))
|
||||
|
||||
@ -314,7 +315,7 @@ class PixelUnpacker(Module):
|
||||
for j in range(4):
|
||||
source_cases[i].append(
|
||||
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.y.eq(y_r),
|
||||
pix.gray.eq(self.sink.data[max_pixel_width*i:max_pixel_width*(i+1)]),
|
||||
)
|
||||
),
|
||||
If(pix.eof,
|
||||
pix.y.eq(self.y_size),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
@ -7,6 +7,11 @@ from collections import namedtuple
|
||||
_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(
|
||||
x_size,
|
||||
y_size,
|
||||
@ -21,7 +26,7 @@ def MonoPixelPacketGenerator(
|
||||
for x in range(x_size):
|
||||
# full white pixel
|
||||
gray = (2**pixel_width) - 1
|
||||
packed += gray << x * pixel_width
|
||||
packed += _switch_bit_order(gray, pixel_width) << x * pixel_width
|
||||
|
||||
# Line marker
|
||||
packet += [
|
||||
@ -41,11 +46,13 @@ def MonoPixelPacketGenerator(
|
||||
|
||||
for i in range(words_per_image_line):
|
||||
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
|
||||
packet.append(
|
||||
_WORDLAYOUT(
|
||||
data=C(serialized, word_width), k=Replicate(0, 4), stb=1, eop=eop
|
||||
),
|
||||
_WORDLAYOUT(data=Cat(word), k=Replicate(0, 4), stb=1, eop=eop),
|
||||
)
|
||||
|
||||
return packet
|
||||
|
@ -43,7 +43,7 @@ class _LogFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
|
||||
|
||||
class _Model(QtCore.QAbstractItemModel):
|
||||
def __init__(self):
|
||||
def __init__(self, palette):
|
||||
QtCore.QAbstractTableModel.__init__(self)
|
||||
|
||||
self.headers = ["Source", "Message"]
|
||||
@ -58,11 +58,16 @@ class _Model(QtCore.QAbstractItemModel):
|
||||
|
||||
self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.SystemFont.FixedFont)
|
||||
|
||||
self.white = QtGui.QBrush(QtGui.QColor(255, 255, 255))
|
||||
self.black = QtGui.QBrush(QtGui.QColor(0, 0, 0))
|
||||
self.debug_fg = QtGui.QBrush(QtGui.QColor(55, 55, 55))
|
||||
self.warning_bg = QtGui.QBrush(QtGui.QColor(255, 255, 180))
|
||||
self.error_bg = QtGui.QBrush(QtGui.QColor(255, 150, 150))
|
||||
self.default_bg = palette.base()
|
||||
self.default_fg = palette.text()
|
||||
self.debug_fg = palette.placeholderText()
|
||||
is_dark_mode = self.default_bg.color().lightness() < self.default_fg.color().lightness()
|
||||
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):
|
||||
if (orientation == QtCore.Qt.Orientation.Horizontal
|
||||
@ -163,13 +168,13 @@ class _Model(QtCore.QAbstractItemModel):
|
||||
elif level >= logging.WARNING:
|
||||
return self.warning_bg
|
||||
else:
|
||||
return self.white
|
||||
return self.default_bg
|
||||
elif role == QtCore.Qt.ItemDataRole.ForegroundRole:
|
||||
level = self.entries[msgnum][0]
|
||||
if level <= logging.DEBUG:
|
||||
return self.debug_fg
|
||||
else:
|
||||
return self.black
|
||||
return self.default_fg
|
||||
elif role == QtCore.Qt.ItemDataRole.DisplayRole:
|
||||
v = self.entries[msgnum]
|
||||
column = index.column()
|
||||
@ -265,7 +270,7 @@ class LogDock(QDockWidgetCloseDetect):
|
||||
cw = QtGui.QFontMetrics(self.font()).averageCharWidth()
|
||||
self.log.header().resizeSection(0, 26*cw)
|
||||
|
||||
self.model = _Model()
|
||||
self.model = _Model(self.palette())
|
||||
self.proxy_model = _LogFilterProxyModel()
|
||||
self.proxy_model.setSourceModel(self.model)
|
||||
self.log.setModel(self.proxy_model)
|
||||
|
@ -15,6 +15,7 @@ import traceback
|
||||
from collections import OrderedDict
|
||||
import importlib.util
|
||||
import linecache
|
||||
import threading
|
||||
|
||||
import h5py
|
||||
|
||||
@ -38,23 +39,42 @@ from artiq import __version__ as artiq_version
|
||||
|
||||
|
||||
ipc = None
|
||||
ipc_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_object():
|
||||
line = ipc.readline().decode()
|
||||
return pyon.decode(line)
|
||||
ipc_lock.acquire()
|
||||
try:
|
||||
line = ipc.readline()
|
||||
finally:
|
||||
ipc_lock.release()
|
||||
return pyon.decode(line.decode())
|
||||
|
||||
|
||||
def put_object(obj):
|
||||
ds = pyon.encode(obj)
|
||||
ipc.write((ds + "\n").encode())
|
||||
ds = (pyon.encode(obj) + "\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 parent_action(*args, **kwargs):
|
||||
request = {"action": action, "args": args, "kwargs": kwargs}
|
||||
put_object(request)
|
||||
reply = get_object()
|
||||
reply = put_and_get_object(request)
|
||||
if "action" in reply:
|
||||
if reply["action"] == "terminate":
|
||||
sys.exit()
|
||||
|
14
flake.lock
generated
14
flake.lock
generated
@ -48,11 +48,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1743494143,
|
||||
"narHash": "sha256-OxeNED91hCgVsbgwRUpmP5BJ4dtilMxF2otGsQ+UBaQ=",
|
||||
"lastModified": 1744341011,
|
||||
"narHash": "sha256-sl8DJxAtqdLUxztNcgUWd9Xp1q271BLc/MfXmJlLkck=",
|
||||
"ref": "refs/heads/master",
|
||||
"rev": "e4f6fbeeebd8d888f2a12d5be027c9394a37ad89",
|
||||
"revCount": 1583,
|
||||
"rev": "800edf35db6ddb05b1f89d13b422b78d12ee024c",
|
||||
"revCount": 1588,
|
||||
"type": "git",
|
||||
"url": "https://git.m-labs.hk/m-labs/nac3.git"
|
||||
},
|
||||
@ -63,11 +63,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1741851582,
|
||||
"narHash": "sha256-cPfs8qMccim2RBgtKGF+x9IBCduRvd/N5F4nYpU0TVE=",
|
||||
"lastModified": 1744098102,
|
||||
"narHash": "sha256-tzCdyIJj9AjysC3OuKA+tMD/kDEDAF9mICPDU7ix0JA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6607cf789e541e7873d40d3a8f7815ea92204f32",
|
||||
"rev": "c8cd81426f45942bb2906d5ed2fe21d2f19d95b7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user